サミー、ステータス、家族。

サミー、ステータス、家族。

若干壊れ気味のソフトウェアネタ、再びです。よろしくお付き合いくださいませ。

私はドラえもんが大好きです。ドラえもんのように、人と自然に話し、感情を持ち、そしてロボット三原則を完全に無視してのび太とケンカをする。。。キャルヴィン博士もびっくりな全人類の夢ですよね?

ところが、実際にはその初歩である『人の話を聞く』ということですら、今の技術だと案外難しいものです。天下のMicrosoftさまですら、Windows Vistaのデモで素晴らしい伝説を残しています。

『親愛なる叔母さん、』と言ったのを、紆余曲折を経てコンピュータが認識した結果は『親愛なる叔父さん、殺し屋に2倍払って全てを始末しちゃってください』です。こんなブラックユーモア旺盛なOSが皆さまのお茶の間に置かれているわけです(笑)。

(Gigazineの記事) http://gigazine.net/index.php?/news/comments/20060809_vista_speech/

先日、『イヴの時間』というアニメを見ていたら、ハウスロイド(ロボットみたいなの)に命令をするとき、『サミー(ロボット名)、ステータス(命令)、家族(内容)』という風に、認識しやすいように話していました。

(イヴの時間) http://www.studio-rikka.com/eve/

実は、人間は何気なく話聞いているように見えても、相手、時間、場所など様々な状況を把握した上で次にくる言葉をある程度予測しているのです。だから、ちょっと発音が微妙だったり、訛っていたり、騒音がうるさい場所でも、正確に内容を認識できるのです。

逆にロボットは、そこまで精度良く予測ができないので、次にくる言葉を、それこそ膨大な言葉の中から選ばなければならないので認識に失敗することが多いわけです。

だから、先ほどのように、『絞り込み検索の要領』で次ぎくる言葉を限定してあげれば簡単に認識できるわけです。

と、いうわけで、『Fy Mascot』に、マイクで話しかけた言葉を認識して対話ができるようにするオプションプログラムを追加します。

まず音声認識のところですが、Microsoftの音声認識機能を使います。

(Microsoft Speech API 5.1 SDK)

http://www.microsoft.com/downloads/en/details.aspx?FamilyID=5e86ec97-40a7-453f-b0ee-6583171b4530&DisplayLang=en

Julius for SAPI プロジェクトのサンプルプログラム(Win32 アプリケーション)が参考になります。

(Julius for SAPI)

http://julius.sourceforge.jp/index.php?q=sapi/index.html

なお、Windows Speech Platform 10.1というのが密かにリリースされているのですが、これだとサンプルプログラムのコードがうまく動作しないのでご注意ください。(これでハマって大変でした。。。)

流れとしては、音声認識の初期化(recognize_initialize)をして待ち受け状態にします。この時に、もし音声を認識したらウィンドウに指定のメッセージを発行するように登録します。今回はWM_RECOEVENTがそれです。

言葉を認識したらウィンドウメッセージが来るのでイベント(recognize_recoevent)を処理します。すべてが終わったら終了(recognize_initialize)します。

なお、基本部分ではないですが、音声認識のルールを動的に変更(recognize_setactive)することで、高い精度での認識を可能にしています。このルールの書き方ですが、上のサンプルにも添付されていますが、xmlファイルで内容はこんな感じになっています。

ルールファイル

<?xml version="1.0" encoding="UTF-8"?>
<GRAMMAR>
<RULE name="S" toplevel="ACTIVE">
  <L>
    <P>
      <RULEREF name="NAME"/>
    </P>
  </L>
</RULE>
<RULE name="NAME" toplevel="INACTIVE">
  <L propname="NAMESTRING">
    <P valstr="サミー">/サミー/さみい;</P>
  </L>
</RULE>
</GRAMMAR>

今回は、それぞれ状態に応じて、name.xml、function.xml、param.xmlの3つのルールを切り替えることで認識の精度を上げています。

ソースコード

//sami.cpp
#include <windows.h>
#include <stdio.h>
#include <sapi.h>
#include <sphelper.h>
#include <process.h>

//宣言
#define WM_RECOEVENT WM_USER+1

//音声認識系
int recognize_initialize( HWND ); //初期化
int recognize_release(); //終了
int recognize_recoevent( HWND , char * , int ); //イベント
//基本部分ではない
int recognize_list( HWND ); //認識した単語をリストに追加
int recognize_setactive( int  , HWND ); //ルールの有効化
 
//ウィンドウ
LRESULT CALLBACK    WndProc(HWND, UINT, WPARAM, LPARAM);
static CComPtr<ISpRecognizer>    m_cpRecoEngine;
static CComPtr<ISpRecoContext>    m_cpRecoCtxt;
static CComPtr<ISpRecoGrammar>    m_cpDictationGrammarName;
static CComPtr<ISpRecoGrammar>    m_cpDictationGrammarFunction;
static CComPtr<ISpRecoGrammar>    m_cpDictationGrammarParam;
static int status;

//本体
int recognize_initialize( HWND hWnd )
{
    unsigned __int64 ullInterest;
    if(!SUCCEEDED(CoInitialize(NULL)))
    {
        return FALSE;
    }
    if(!SUCCEEDED(m_cpRecoEngine.CoCreateInstance(CLSID_SpSharedRecognizer)))
    {
        return FALSE;
    }
    if(!SUCCEEDED(m_cpRecoEngine->CreateRecoContext( &m_cpRecoCtxt )))
    {
        return FALSE;       
    }
    // Set recognition notification for dictation
    if(!SUCCEEDED(m_cpRecoCtxt->SetNotifyWindowMessage( hWnd , WM_RECOEVENT, 0, 0 )))
    {
        return FALSE;
    }   
    ullInterest = SPFEI(SPEI_RECOGNITION) | SPFEI(SPEI_FALSE_RECOGNITION)| SPFEI(SPEI_HYPOTHESIS) | SPFEI(SPEI_SOUND_START) | SPFEI(SPEI_SOUND_END);
    if(!SUCCEEDED(m_cpRecoCtxt->SetInterest(ullInterest, ullInterest)))
    {
        return FALSE;
    }
    //名前のルール読み込み
    if(!SUCCEEDED(m_cpRecoCtxt->CreateGrammar( 0, &m_cpDictationGrammarName )))
    {
        return FALSE;
    }
    if(!SUCCEEDED(m_cpDictationGrammarName->LoadCmdFromFile(L".\\name.xml",SPLO_STATIC)))
    {
        return FALSE;
    }
    //関数のルール読み込み
    if(!SUCCEEDED(m_cpRecoCtxt->CreateGrammar( 0, &m_cpDictationGrammarFunction )))
    {
        return FALSE;
    }
    if(!SUCCEEDED(m_cpDictationGrammarFunction->LoadCmdFromFile(L".\\function.xml",SPLO_STATIC)))
    {
        return FALSE;
    }
    if(!SUCCEEDED(m_cpRecoCtxt->CreateGrammar( 0, &m_cpDictationGrammarParam )))
    {
        return FALSE;
    }
    //パラメータのルール読み込み
    if(!SUCCEEDED(m_cpDictationGrammarParam->LoadCmdFromFile(L".\\param.xml",SPLO_STATIC)))
    {
        return FALSE;
    }
    //とりあえず非アクティブにしておく
    if(!SUCCEEDED(m_cpDictationGrammarName->SetDictationState( SPRS_INACTIVE )))
    {
        return FALSE;
    }
    if(!SUCCEEDED(m_cpDictationGrammarFunction->SetDictationState( SPRS_INACTIVE )))
    {
        return FALSE;
    }
    if(!SUCCEEDED(m_cpDictationGrammarParam->SetDictationState( SPRS_INACTIVE )))
    {
        return FALSE;
    }
    return TRUE;
}
int recognize_setactive( int status , HWND hWnd )
{
    switch(status)
    {
    case 1:
        {
            m_cpDictationGrammarName->SetRuleState(NULL, NULL, SPRS_ACTIVE );
            m_cpDictationGrammarFunction->SetRuleState(NULL, NULL, SPRS_INACTIVE );
            m_cpDictationGrammarParam->SetRuleState(NULL, NULL, SPRS_INACTIVE );
            SetWindowText( GetDlgItem( hWnd,1) , "。。。" );
            return TRUE;
        }
        break;
    case 2:
        {
            m_cpDictationGrammarName->SetRuleState(NULL, NULL, SPRS_INACTIVE );
            m_cpDictationGrammarFunction->SetRuleState(NULL, NULL, SPRS_ACTIVE );
            m_cpDictationGrammarParam->SetRuleState(NULL, NULL, SPRS_INACTIVE );
            SetWindowText( GetDlgItem( hWnd,1) , "はい、何でしょうかご主人様。" );
            return TRUE;
        }
        break;
    case 3:
        {
            m_cpDictationGrammarName->SetRuleState(NULL, NULL, SPRS_INACTIVE );
            m_cpDictationGrammarFunction->SetRuleState(NULL, NULL, SPRS_INACTIVE );
            m_cpDictationGrammarParam->SetRuleState(NULL, NULL, SPRS_ACTIVE );
            SetWindowText( GetDlgItem( hWnd,1) , "はい。" );
            return TRUE;
        }
        break;
    case 4:
        {
            m_cpDictationGrammarName->SetRuleState(NULL, NULL, SPRS_INACTIVE );
            m_cpDictationGrammarFunction->SetRuleState(NULL, NULL, SPRS_INACTIVE );
            m_cpDictationGrammarParam->SetRuleState(NULL, NULL, SPRS_INACTIVE );
            SetWindowText( GetDlgItem( hWnd,1) , "かしこまりました。" );
            return TRUE;
        }
        break;
    default:
        {
            m_cpDictationGrammarName->SetDictationState( SPRS_ACTIVE );
            m_cpDictationGrammarFunction->SetDictationState( SPRS_ACTIVE );
            m_cpDictationGrammarParam->SetDictationState( SPRS_ACTIVE );
            return TRUE;
        }
        break;
    }
}
int recognize_release()
{
    if(m_cpRecoCtxt)
    {
        m_cpRecoCtxt->SetNotifySink(NULL);
        m_cpRecoCtxt.Release();
    }
    m_cpDictationGrammarName.Release();
    m_cpDictationGrammarFunction.Release();
    m_cpDictationGrammarParam.Release();
    CoUninitialize();
    return TRUE;
}
int recognize_event( HWND hWnd )
{
    USES_CONVERSION;
    CSpEvent cspevent;
    static int m_bInSound = FALSE;
    static int m_bGotReco = FALSE;
    while(cspevent.GetFrom(m_cpRecoCtxt)==S_OK)
    {
        switch (cspevent.eEventId)
        {
        case SPEI_SOUND_START:
            {
                m_bInSound = TRUE;
                return TRUE;
            }
            break;
        case SPEI_SOUND_END:
            {
                if(m_bInSound)
                {
                    m_bInSound = FALSE;
                    if(!m_bGotReco)
                    {
                        //ノイズなので失敗
                    }
                    m_bGotReco = FALSE;
                }
                return TRUE;
            }
            break;
        case SPEI_RECOGNITION:
            {
                // There may be multiple recognition results, so get all of them
                wchar_t *buffer = NULL;
                m_bGotReco = TRUE;
                if(FAILED(cspevent.RecoResult()->GetText(SP_GETWHOLEPHRASE, SP_GETWHOLEPHRASE, TRUE, &buffer, NULL)))
                {
                    //関数の失敗
                    return FALSE;
                }
                // Concatenate a space onto the end of the recognized word
                SendMessageW( GetDlgItem(hWnd,2), LB_ADDSTRING, 0L, (LPARAM)buffer );
                status = status + 1;
                recognize_setactive( status , hWnd );
                return TRUE;
            }
            break;
        default:
            return FALSE;
            break;
        }
    }
    return FALSE;
}
int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow )
{
    MSG msg;
    WNDCLASSEX wcex;
    HWND hWnd;
    memset( &wcex, 0, sizeof(WNDCLASSEX) );
    wcex.cbSize            = sizeof(WNDCLASSEX);
    wcex.style            = CS_HREDRAW | CS_VREDRAW;
    wcex.lpfnWndProc    = WndProc;
    wcex.hInstance        = hInstance;
    wcex.hCursor        = LoadCursor(NULL, IDC_ARROW);
    wcex.hbrBackground    = (HBRUSH)(COLOR_WINDOW+1);
    wcex.lpszClassName    = "WindowClass";
    RegisterClassEx(&wcex);
    hWnd = CreateWindow( "WindowClass", "Title", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, 400 , 300 , NULL, NULL, hInstance, NULL);
    if(!hWnd)
    {
        return FALSE;
    }
    ShowWindow(hWnd, nCmdShow);
    UpdateWindow(hWnd);
    // メイン メッセージ ループ:
    while (GetMessage(&msg, NULL, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    return (int) msg.wParam;
}
//
//  関数: WndProc(HWND, UINT, WPARAM, LPARAM)
//
//  目的:  メイン ウィンドウのメッセージを処理します。
//
//  WM_COMMAND    - アプリケーション メニューの処理
//  WM_PAINT    - メイン ウィンドウの描画
//  WM_DESTROY    - 中止メッセージを表示して戻る
//
//
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    case WM_CREATE:
        {
            //ステータス表示
            CreateWindowEx( WS_EX_STATICEDGE, "STATIC", "", WS_CHILD|WS_VISIBLE|LBS_NOTIFY, 10, 10, 360, 30, hWnd, (HMENU)1, ((CREATESTRUCT*)lParam)->hInstance, NULL );
            CreateWindowEx( WS_EX_STATICEDGE, "LISTBOX", "", WS_CHILD|WS_VISIBLE|LBS_NOTIFY, 10, 50, 360, 100, hWnd, (HMENU)2, ((CREATESTRUCT*)lParam)->hInstance, NULL );
            //レコグナイズ初期化
            if(!recognize_initialize( hWnd ))
            {
                SetWindowText( GetDlgItem(hWnd,1) , "SAPI初期化失敗" );
            }
            else
            {
                status = 1;
                recognize_setactive( 1 , hWnd );
                SetWindowText( GetDlgItem(hWnd,1) , "準備完了" );
            }
            return TRUE;
        }
        break;
    case WM_COMMAND:
        {
            unsigned short wmId, wmEvent;
            wmId    = LOWORD(wParam);
            wmEvent = HIWORD(wParam);
            // 選択されたメニューの解析:
            switch (wmId)
            {
            case 1:
                {
                    DestroyWindow(hWnd);
                    return TRUE;
                }
                break;
            default:
                return DefWindowProc(hWnd, message, wParam, lParam);
                break;
            }
        }
        break;
    case WM_RECOEVENT:
        {
            if(recognize_event( hWnd ))
            {
            }
            return TRUE;
        }
        break;
    case WM_PAINT:
        {
            PAINTSTRUCT ps;
            HDC hDC;
            hDC = BeginPaint(hWnd, &ps);
            // TODO: 描画コードをここに追加してください...
            EndPaint(hWnd, &ps);
            return TRUE;
        }
        break;
    case WM_DESTROY:
        {
            recognize_release();
            PostQuitMessage(0);
            return TRUE;
        }
        break;
    default:
        return DefWindowProc(hWnd, message, wParam, lParam);
        break;
    }
}

早速コンパイルして実行してみます。

1.成功していれば、起動すると音声認識用のパレット(上の部分)が出てくるはずです。

イメージ

2.最初は名前のルールが有効なので、名前として認識します。
認識されたら、次はファンクションのルールを有効にします。

イメージ

3.ファンクションとして『ステータス』が認識されたので、
こんどはパラメータのルールが有効になります。

イメージ

4.パラメータ『家族』 が認識されて、終了します。

イメージ

今回もとりあえず動かしてみる的なノリなので、このままオプションプログラムとして『Fy Mascot』に実装します。

ウィンドウに文字を表示するかわりに、名前はオモヒカネにNOTICE-MESSAGEを送ってセリフを言った上でエディットボックスを表示して待ち受け状態にします。対応するスクリプト『message_recognize.xml』はそのまま『Fy Mascot』に添付してリリースしているので興味があった方は見てみてください。 さらにファンクション、パラメータはマスコット本体にSYSTEM-SETTEXTを送って、エディットボックスに入力します。

あとは、ほとんどコードに変更はありません。

ソースコード

int gicp_noticemessage()
{
    char master[256];
    char follower[256];
    char witchcraft[256];
    char incantation[512];
    char *buffer;
    int length;
    /* 各要素を作る */
    sprintf( master , "<place>OPTION</place><detail><name>SAMPLE</name></detail>" );
    sprintf( follower , "<place>CHARACTER</place><detail></detail>" );
    sprintf( witchcraft , "<class>NOTICE</class><order>MESSAGE</order>" );
    sprintf( incantation , "<sender>おんせーにんしき</sender><string>音声認識を開始しました。< /string><userdata><exclusive><name>RECOGNIZE</name><attribute>RECEIVE</attribute></exclusive></userdata>" );
    /* 各データを元にGICP文字列を作る */
    buffer = gicp_setstring( master , follower , witchcraft , incantation );
    length = strlen(buffer)+1;
    /* GICPを送信!*/
    gicp_sendcom( &buffer , &length );
    /* メモリ解放 */
    free( buffer );
    return TRUE;
}
int gicp_systemsettext( char *string )
{
    char master[256];
    char follower[256];
    char witchcraft[256];
    char incantation[512];
    char *buffer;
    int length;
    /* 各要素を作る */
    sprintf( master , "<place>OPTION</place><detail><name>SAMPLE</name></detail>" );
    sprintf( follower , "<place>MASCOT</place><detail></detail>" );
    sprintf( witchcraft , "<class>SYSTEM</class><order>SETTEXT</order>" );
    sprintf( incantation , "<data>%s</data>" , string );
    /* 各データを元にGICP文字列を作る */
    buffer = gicp_setstring( master , follower , witchcraft , incantation );
    length = strlen(buffer)+1;
    /* GICPを送信!*/
    gicp_sendcom( &buffer , &length );
    /* メモリ解放 */
    free( buffer );
    return TRUE;
}

なお、実行にあたっては、オプションプログラムではなく、『Fy Mascot』のプログラムと同じフォルダにname.xml,function.xml,param.xmlの各ファイルをコピーしておいてください。

さて実行してみます!

メニューから音声認識用のウィンドウを起動させます。

イメージ

1.ちゃんと、音声認識パレットがでてきました。オフになっているのでオンにします。

イメージ

2.名前を呼びます。そのまま移植したのでサミーです。。。
さきほどはセリフをウィンドウの上部に表示していましたが、
ちゃんとマスコットが反応していますね。
ウィンドウの上部には現在の状態を表示するようにしました。

イメージ

3.ファンクション受付状態でファンクションであるステータスを認識して、
エディットボックスに入力し、パラメータ受付状態になります。
キャラクターはとりあえず『はい』と返事だけしています。

イメージ

4.パラメータである家族を認識して終了します。
キャラクターは『かしこまりました』と言っています。(もちろん何もしてくれませんが。)

イメージ

今回は壮大な風呂敷を広げましたが、実際にやっていることはルールを使ってうまく音声認識しているだけです。

ですが、ざっくばらんな会話を楽しむことはできませんが、『マスコットに何かしてもらう』的な用途では結構使えるテクニックだと思います。ルールは動的に変更できるので、対応しているファンクションの一覧や、選択したファンクションの対応しているパラメータ一覧を動的に切り替えることで、作り込み次第でかなり可能性は広がります。

マスコット側がそれに応じて対応できるようにすれば、音声で何かやってもらうことが比較的簡単にできるかもしれません。