私だけのアクション「My アクション」の作り方~ SOCKET SERVER 'HAL'

ソケットサーバー「HAL」の楽しみは、自分で自由に機能を簡単に追加できる事です。フレームワークに沿ってほんの少し記述をするだけで、とても簡単にあなたの望む動作をするアプリケーションが作れます。

ここではまず、音声で動作する簡単なテストアクションを作ってみましょう。

私だけのアクション「My アクション」の作り方

 ソケットサーバー「HAL」の楽しみは、自分で自由に機能を簡単に追加できる事です。フレームワークに沿ってほんの少し記述をするだけで、とても簡単にあなたの望む動作をするアプリケーションが作れます。

 ここではまず、音声で動作する簡単なテストアクションを作ってみましょう。

スケルトンの作成

 MyAction を作るには、bakepi を使ってアクションの雛形であるスケルトンファイルを生成し、そこにコードを記述していくと良いでしょう。

NetBeans プロジェクトの作成

 プログラムを記述するのはテキストエディタでも構いませんが、NetBeans IDE を使うとコード入力のサポートが効いて便利です。NetBeans の導入の仕方は Raspberry Pi で 便利な開発環境「NetBeans」を導入 を参照して下さい。

 初めて NetBeans を使う時には、以下のように最初に HAL のソースを元に新規プロジェクトを作成する必要があります。

 まず、NetBeans を立ち上げたら ファイル > 新規プロジェクト で新規プロジェクトダイアログを開き、「PHP」「既存のソースを使用する PHP アプリケーション」を選んで下さい。これで、HAL のソースディレクトリを対象としてプロジェクトが生成されるように設定できます。

新規プロジェクト

 「次 > 」をクリックして下さい。

 「名前と場所」ダイアログが開きます。ここで、ソース・フォルダに HAL ディレクトリまでのパスを指定して下さい。

ソースフォルダの指定

 また、このプロジェクト用の設定ファイルを HAL ディレクトリとは別の場所に配置(ユーザーディレクトリ内)に作成するため、「NetBeans のメタデータを別のディレクトリに配置」にチェックを入れて下さい。(メタデータ・フォルダに入力されているパスを確認しておいて下さい)

 「次 > 」をクリックして下さい。

 「実行構成」ダイアログが開きます。ここで、WEB アプリケーションを実行する場合の設定を行います。通常は HAL のインストール時に、Apache のドキュメントルートを /HAL/webapps/public に設定してると思いますので、プロジェクト URL は、

http://localhost/

WEB 設定

としてください。localhost とは、お使いのコンピューター自身を指します。

 「終了」をクリックして下さい。プロジェクトが生成されます。

プロジェクトが開きました

 なお、右側に開いている「開始ページ」は閉じてしまって構いません。NetBeans 起動時に毎回開かれるのが邪魔な場合は、右上にある「起動時に表示」のチェックボックスを外して閉じて下さい。

アクションの種類

 アクションには 3 つの種類があります。

  • voice アクション

 音声命令で実行されるアクションです。音声認識エンジン Julius が必要です。

  • net アクション

 他のコンピューターやプロセスからのソケット通信を使った命令で実行されるアクションです。

  • sensor アクション

 Raspberry Pi に接続されたセンサー等の値を検査して処理を行うアクションです。HAL の実行インターバル毎に定期的に実行されます。

 このページでは音声認識による voice アクションを作成していきましょう。

スケルトンの生成

 さて、では voice アクションのスケルトンを作成します。ターミナルで、以下のコマンドを実行して下さい。

sudo bakepi -s voice chat

 -s オプションは、アクション・スケルトン生成用のオプションです。続けて、voice を指定し、最後にアクション名を記述子ます。アクション名はキャメルケースにしてください。

参考)キャメルケース(wikipedia)

 上記コマンドを実行すると、/HAL/userdata/dev/voice 内に、MyChat 用のファイルが生成されます。

生成されたスケルトン

 languages フォルダ内に生成されているのは応答メッセージ用の雛形ファイルですが、これは、/HAL/setting/HALSetting.php の USE_LANGUAGE に定義されている言語ロケールの数だけファイルが作成されます。

音声認識定義を作成

 スケルトンが生成できたら、まず、どんな言葉に対して応答させたいのかを考えてみましょう。

  • こんにちは
  • いい天気だね
  • 今何時?

 まずは、この 3 つに対して応答させます。

 /voice/julius/MyChat.julius.yml を開いて下さい。このファイルは、YAML(ヤムル)という汎用的なデータ記述形式になっています。YAML はインデント(字下げ)で階層構造を記述するので記述がとても簡単です。

## MyChat Julius YAML.
## @NAME はHALSettingで設定した名前に置換えられます
## @KANA はHALSettingで設定した名前の読みに置換えられます
---
rules:
- "RULE_ DO"
alias:
RULE_:
- "RULE"
- "RULE WO"
words:
RULE:
-
word: "ルール"
kana: "ルール"
WO:
-
word: "を"
kana: "ヲ"
DO:
-
word: "実行"
kana: "ジッコウ"

 ではまず、words に単語を登録していきましょう。

 先程書いたとおり、YAML はインデントで階層構造を表すので、インデントは統一する必要があります。

 このファイルの字下げは半角スペース 4 つで1ブロックになっています。NetBeans ではデフォルトで、TAB キーで半角スペース 4 つとして入力されます。

 ではまず「こんにちは」から登録します。

words:
AISATU:
-
word: "こんにちは"
kana: "コンニチワ"

 このようになります。kana には認識させたい言葉をダブルクオートで囲んでカタカナで記述子してください。(和文と英文の切り替えは Ctrl + J で出来るかと思います)

 単語が登録できたら、次は roules です。ここで、単語を組み合わせた文法を登録します。今回使用するのは「こんにちは」だけなので、登録は以下のようになります。

rules:
- "AISATSU"
alias:
words:
AISATSU:
-
word: "こんにちは"
kana: "コンニチワ"

 これで Julius が「こんにちは」を認識できるようになります。

 続けて、「いい天気だね」を登録してみましょう。ただし、そのまま「いい天気だね」を登録するのではなく、「だ」と「ね」を分けてみます。

  :
words:
AISATSU:
-
word: "こんにちは"
kana: "コンニチワ"
TENKI:
-
word: "いい天気"
kana: "イイテンキ"
DA:
-
word: "だ"
kana: "ダ"
NE:
-
word: "ね"
kana: "ネ"

 単語が登録できたら、先ほどと同様にルールを定義しますが、今回は alias も併せて使ってみます。

rules:
- "AISATSU"
- "TENKI DANE_"
alias:
DANE_:
- "DA"
- "NE"
- "DA NE"
words:
AISATSU:
-
word: "こんにちは"
kana: "コンニチワ"
TENKI:
-
word: "いい天気"
kana: "イイテンキ"
DA:
-
word: "だ"
kana: "ダ"
NE:
-
word: "ね"
kana: "ネ"

 上記のように DANE_ というエイリアスを作成し、そこに「DA」「NE」「DA NE」を登録しています。rules では、このエイリアスを使って「TENKI DANE_」を登録しました。(なお、エイリアスは必ず最後にアンダースコア _ を付けなければいけないわけではありません)

 こうすることで、「いい天気だ」「いい天気ね」「いい天気だね」の 3 つの言葉に対して、1 つのルールで対応できるようになります。

 さて、最後に残った「今何時?」も登録してしまいましょう。

rules:
- "AISATSU"
- "TENKI DANE_"
- "WHATTIME"
alias:
DANE_:
- "DA"
- "NE"
- "DA NE"
words:
AISATSU:
-
word: "こんにちは"
kana: "コンニチワ"
TENKI:
-
word: "いい天気"
kana: "イイテンキ"
DA:
-
word: "だ"
kana: "ダ"
NE:
-
word: "ね"
kana: "ネ"
WHATTIME:
-
word: "今何時?"
kana: "イマナンジ"

 出来たでしょうか? ファイルを保存して閉じて下さい。

 正しく記述できたか調べるため、コンパイルしてみましょう。julius.yml のコンパイルは、bakepi コマンドに -j オプションを付けて実行します。

$ sudo bakepi -j MyChat

process: MyChat.julius

MyChat.grammar is created.
MyChat.voca is created.
/var/www/HAL/system/../userdata/dev/voice/julius/MyChat.grammar has 6 rules
/var/www/HAL/system/../userdata/dev/voice/julius/MyChat.voca has 7 categories and 7 words
---
Now parsing grammar file
Now modifying grammar to minimize states[1]
Now parsing vocabulary file
Now making nondeterministic finite automaton[8/8]
Now making deterministic finite automaton[8/8]
Now making triplet list[8/8]
7 categories, 8 nodes, 11 arcs
-> minimized: 6 nodes, 9 arcs
---
generated: /var/www/HAL/system/../userdata/dev/voice/julius/MyChat.dfa /var/www/HAL/system/../userdata/dev/voice/julius/MyChat.term /var/www/HAL/system/../userdata/dev/voice/julius/MyChat.dict

+++ Success.

 上記のようにエラーが出力されず「+++ Success.」が表示されればコンパイル完了です。

YAML の補足事項

 YAML には予約語と呼ばれる語がいくつかあり、この予約後は識別子として利用できません。例えば、yes、no、true、false といった語です。このうち「no」について、日本語の助詞である「の」と重複してしまうため、そのままでは識別子に使えません。この場合はクオートで囲って文字列であることを明示する必要があります。

例)

words:
'NO':
-
word: "の"
kana: "ノ"

アクション YAML で音声認識に対する処理を定義

 さて次は、音声認識結果を受け取る「アクション YAML」を編集します。MyChat.yaml を開いて下さい。

## MyChat YAML.
---
-
type: "ルールを?実行"
do: "exec"

 このようにひな形が作成されています。先程の 3 つのルールに対する処理を記述していきましょう。

## MyChat YAML.
---
-
type: "こんにちは"
do: "hello"

 このようになります。Julius からは音声認識結果として word に登録した文字列「こんにちは」が送られてくるので、これを type に指定し、実際に行う処理を記述したメソッド名を do に指定します。上記の場合、MyChat.php に記述されている MyChat クラスのクラスメソッド hello() が実行されます。(hello() メソッドはこの後作成します)

 次は、「いい天気だね」です。

## MyChat YAML.
---
-
type: "こんにちは"
do: "hello"
-
type: "いい天気(だ|ね|だね)"
do: "fine_day"

 (だ|ね|だね) は正規表現で グループ と呼びます。丸括弧で囲い、パイプで区切って単語を列挙すると、列挙した単語のどれでもマッチさせることができます。これにより、Julius からの音声認識結果が「いい天気だ」「いい天気ね」「いい天気だね」のどれでもマッチさせ、fine_day() メソッドを実行させることができます。

 あとは「今何時?」を登録してしましましょう。

## MyChat YAML.
---
-
type: "こんにちは"
do: "hello"
-
type: "いい天気(だ|ね|だね)"
do: "fine_day"
-
type: "今何時?"
do: "what_time_is_it_now"

 これで、アクション YAML の登録は完了です。では、do で指定されているメソッドを実装していきましょう。

アクション PHP の記述

 メソッドの実装は、アクション PHP で行います。MyChat.php ファイルを開いて下さい。次のような雛形が作成されています。

<?php
/* *
*
* このアクションについての説明をここに書いて下さい
*
* */
namespace Feijoa\HAL\Action\Voice;
use Feijoa\HAL as HALSYS;

class MyChat extends HALSYS\Action\BaseAction
{

/**
* 呼び出し元クラス名を返すImplementsメソッド
*
* @return string // クラス名
*/
public static function getClass(){ return __CLASS__; }

/**
* Readyイベントメソッド
*
* @param \Feijoa\HAL\HALDto $dto
*/
//public static function ready(HALSYS\HALDto $dto){}

/**
* インターバル毎のイベント
*
* @param \Feijoa\HAL\HALDto $dto
*/
//public static function sensor(HALSYS\HALDto $dto){}

/**
* この関数の説明をここに書いて下さい。関数名は自由に変える事ができます。
* 変更した関数名をMyChat.ymlの do プロパティに記述することで、この関数をあなたの望む指示に割り当てられます
*
* @param \Feijoa\HAL\HALDto $dto
* @param Resource $read_sock 接続ソケットのリソース
* @param array $args 音声認識結果のリスト
*/
public static function exec(HALSYS\HALDto $dto, $read_sock, $args)
{
// ここにあなたのコードを書いて下さい

self::say($dto, "my_test_code", "テストコードを実行しました。");
echo "My code has done.\n";
}
}

 先頭の <?php という記述は、ここから PHP のプログラムコードが始まります、というタグですので、省かないで下さい。なお、末尾に ?> という閉じタグを記述すると、プログラムコードの終端を指定できますが、HTML コードを同じファイル内に記述するのでない限り、閉じタグは記述すべきではありません

 また、getClass() というメソッドは HAL では必須メソッドとなっていますので、これも削除しないで下さい。

 さて、ではまず「こんにちは」に対する処理を行うメソッド hello() を記述します。次の exec() メソッドを書き換えてしまいましょう。

    public static function exec(HALSYS\HALDto $dto, $read_sock, $args)
{
// ここにあなたのコードを書いて下さい

self::say($dto, "my_test_code", "テストコードを実行しました。");
echo "My code has done.\n";
}

  ↓

    public static function hello(HALSYS\HALDto $dto, $read_sock, $args)
{
self::say($dto, "say_hello", "はい、こんにちは。");
}

 以上で、「こんにちは」という音声認識結果に対する応答メッセージ定義は終了です。

 "say_hello" というのは、応答音声データをファイルとして書き出す時の識別子です。この文字列は、My アクションの中で一意であれば何でも構いません。(英数字、ハイフン、アンダースコアのみを推奨します)

 HAL はデフォルトで Open JTalk を利用する用に設定されますが、Open JTalk は音声データを生成するのに若干時間がかかります。このため、一度発声した言葉については音声データをファイルとして保存しておき、再び同じ言葉を発声する必要があった場合、Open JTalk での音声データ作成をスキップし、前回作成した音声データファイルを再生して処理速度を向上させています。

 なお、HAL では Open JTalk 以外のスピーチエンジンも選択いただけるようになっています。現在は eSpeak と AquesTalk に対応していますし、他に良いスピーチエンジンが見つかった場合は順次対応していきます。

 最後の "はい、こんにちは。" はデフォルトの返答です。この後、応答メッセージ YAML を記述しますが、応答メッセージ YAML に "say_hello" 用の応答メッセージが定義されていない場合、ここで記述した "はい、こんにちは。" が発声されます。

 引き続き、同様に fine_day() メソッドも実装します。

 ここではまだ HAL は、現在の天気を取得する手段がないので、常に決まったメッセージを返すようにしましょう。

    /**
* 「こんにちは」に対する応答
*
* @param \Feijoa\HAL\HALDto $dto
* @param Resource $read_sock 接続ソケットのリソース
* @param array $args 音声認識結果のリスト
*/
public static function hello(HALSYS\HALDto $dto, $read_sock, $args)
{
self::say($dto, "say_hello", "はい、こんにちは。");
}

/**
* 「いい天気だね」に対する応答
*
* @param \Feijoa\HAL\HALDto $dto
* @param Resource $read_sock 接続ソケットのリソース
* @param array $args 音声認識結果のリスト
*/
public static function fine_day(HALSYS\HALDto $dto, $read_sock, $args)
{
self::say($dto, "it_s_a_fine_day", "本当にいい天気ですね。洗濯物を片付けてしまいましょう。");
}

 さて、最後は「今何時?」に対する応答処理です。以下のメソッドを fine_day() メソッドの後に追加して下さい。

    /**
* 「今何時?」に対する応答
*
* @param \Feijoa\HAL\HALDto $dto
* @param Resource $read_sock 接続ソケットのリソース
* @param array $args 音声認識結果のリスト
*/
public static function what_time_is_it_now(HALSYS\HALDto $dto, $read_sock, $args)
{
$ap = date("a"); // am or pm を取得

$am_pm = "午後";
if($ap === "am"){
$am_pm = "午前";
}

$hour = date("H"); // 時刻(24時間)を取得
$minutes = (int)date("i"); // 分(0埋め)を取得

// 現在の言語取得
$lang_type = HALSYS\I18N::getFunction($dto, $dto->lang);
switch($lang_type)
{
case "common":
$say_hour = ((int)date("g") === 12)? "0" : (int)date("g");
if($minutes === 0)
{
// 0分の時は分は読まない
$word = "{$am_pm} {$say_hour} o'clock";
self::say($dto, "what_time_is_it_just_now", $word, array($say_hour, $ap));
} else {
// 0分以外
$word = "{$say_hour} {$minutes} {$am_pm}";
self::say($dto, "what_time_is_it_now", $word, array($say_hour, $minutes, $ap));
}
break;

case "japanese";
$say_hour = ((int)date("g") === 12)? "れい" : (int)date("g"); // 時刻(12時間)を取得、0時を「れいじ」に読み替え
$say_minutes = ($minutes === 30)? "半" : $minutes . "分"; // 30分を「半」に読み替え

if($minutes === 0)
{
// 0分の時は分は読まない
$word = "{$am_pm} {$say_hour}時です";
self::say($dto, "what_time_is_it_just_now", $word, array($say_hour, $am_pm));
} else {
// 0分以外
$word = "{$am_pm} {$say_hour}時{$say_minutes}です";
self::say($dto, "what_time_is_it_now", $word, array($say_hour, $say_minutes, $am_pm));
}
break;
}
}

 PHP での日付・時刻取得メソッドは date() が便利です。引数として特定の文字列を渡すと、それに対応する日付・時刻の値が取得できます。

 例えば現在の日時を 2016/12/12 15:15:00 の形式で表示したい場合は、

echo date("Y/m/d H:i:s");

 となります。引数に渡す文字については、以下の PHP マニュアルを参考にしてください。

参考)date|PHP マニュアル

 HALSYSI18N::getFunction は、現在の言語設定に対して実行するメソッドを分けたい場合などに利用します。このメソッドを実行すると、現在の設定が ja_JP、ja_KS の場合は japanese、それ以外は common という文字列が返ります。(※どんなロケールの時にどんな文字列を返すかは、/HAL/system/class/I18N.php の getFunction() メソッドで設定できます)

 こうして取得した文字列を元に、生成する音声メッセージを switch で変えています。

 なお、今回の self::say() メソッドですが、最後に array($say_hour, $say_minutes, $am_pm) のように、配列を渡しています。こうすることで生成する音声データファイルに、渡した配列の要素を反映させることができます。

 次の項で、その使い方を見てみましょう。MyChat.php を保存して閉じて下さい。

応答メッセージ YAML の定義

 これでも一応動作しますが、あと一手間、応答メッセージ YAML を記述しましょう。この応答メッセージ YAML は多言語対応と動的パラメター対応を行ってくれます。

 まず、日本標準語用の /voice/languages/MyChat.ja_JP.yml を編集します。以下のような雛形が作成されています。

## MyChat: ja_JP
---
prop: "読み上げる文字列をここに書いて下さい"
my_test_code: "テストコードを実行しました。"

 では、先程の「こんにちは」と「いい天気だね」に対する応答メッセージを記述しましょう。

## MyChat: ja_JP
---
say_hello: "はい、こんにちは。"
it_s_a_fine_day: "本当にいい天気ですね。洗濯物を片付けてしまいましょう。"

 このようになります。次は「今何時?」用の応答メッセージです。

## MyChat: ja_JP
---
say_hello: "はい、こんにちは。"
it_s_a_fine_day: "本当にいい天気ですね。洗濯物を片付けてしまいましょう。"
what_time_is_it_now: ":@3 :@1時 :@2です"
what_time_is_it_just_now: ":@2 :@1時です"

 :@1 といった記述が出てきました。これは先程

self::say($dto, "what_time_is_it_now", $word, array($say_hour, $say_minutes, $am_pm));

 のようにして最後に渡した array の要素を展開するためのプレースホルダです。上記の例ですと、what_time_is_it_now に対しては ":@3 :@1時 :@2です" が用いられ、プレースホルダについて、渡された配列が順番に当て嵌められて音声データが作成されます。

  • :@1 = $say_hour
  • :@2 = $say_minutes
  • :@3 = $am_pm

 このように、プレースホルダの数字は 1 から始まります。(1 オリジン)

 これで一通り動かすことができますが、先程述べたとおり、応答メッセージ YAML は多言語対応されています。今作成した ja_JP を使って ja_KS(関西弁)、en_US(アメリカ英語)についても応答メッセージを定義してみましょう。

MyChat.ja_KS.yml

say_hello:         "はい、こんにちは。"
it_s_a_fine_day: "本当にええ天気やね。洗濯物、片付けてまいましょう。"
what_time_is_it_now: ":@3 :@1時 :@2やで"
what_time_is_it_just_now: ":@2 :@1時やで"

MyChat.en_US.yml

say_hello:         "hello."
it_s_a_fine_day: "It's a really fine day. Let's clean up the laundry."
what_time_is_it_now: ":@1 :@2 :@3"
what_time_is_it_just_now:  ":@2 :@1 o'clock"

※私は関西弁ネイティブでも英語ネイティブでもないので、メッセージの間違いは大目に見て下さい。

さぁ、動かしてみよう!

 さて、これで My アクションの作成が終わりました。では HAL を起動してアクションをテストしてみましょう。

sudo hal restart

 どうでしょう? HAL は上手く応答してくれたでしょうか?

 なお、HAL には予め規定の音声認識と応答メッセージが登録されています。動画の「ハル、聞こえる?」「ハル、終了」等は規定の音声認識と応答メッセージで、これらは Voice_System アクション、及び Voice_Common アクションに登録されています。

 また「今何時?」で英語対応を行っていますが、言語設定を英語に切り替えるには /HAL/setting/HALSetting.php の

const USE_LANGUAGE = array("ja_JP", "ja_KS", "en_US");

const USE_LANGUAGE = array("en_US", "ja_JP", "ja_KS");

のように、第一言語を en_US に設定するか、

sudo bakepi -g voice language ad7862413199b69d7596e90aec92ffb405cfdadc

を実行して 言語切り替えアクション をインストールし、「ハル、言語、日本語」「ハル、言語、関西弁」「ハル、言語、英語」と命令して言語設定を切り替えてください。(bakepi -g の最後のチェックサムは変更になっている場合があります。サンプルアクションリストを参照して最新の物を入力してください)

My アクションが完成したらインストールしよう

 My アクションが完成したら、-gm オプションをつけた bakepi コマンドで、正規のアクションとして HAL にインストールできます。

sudo bakepi -gm voice chat

 上記コマンドを実行すると、My アクションのファイルは

  • /HAL/actions
  • /HAL/userdata/julius
  • /HAL/userdata/voice/languages

の 3 つのディレクトリに移動されます。

 正規のアクションとしてインストールされた My アクションを削除するには、通常のアクション同様 -r オプションを使います。

sudo bakepi -r voice MyChat

 -e オプションを付けてエクスポートすることも可能です。バックアップなどに使って下さい。

sudo bakepi -e voice MyChat

My アクションの無効化

 なお、/HAL/setting/HALSetting.php の最初にある

const DEVELOP_MODE = true;

false に設定することで、My アクションを全て読み込まずに HAL を起動することができます。My アクションのプログラムを間違えて上手く起動しない場合に、一時的に My アクションを無効にして HAL を起動する場合などに利用して下さい。

自由に My アクションを作り変えてみよう!

 このように、アクションの登録はとても簡単です。PHP コードに触れたことがある方はすぐにアクションを登録できるでしょう。

 上記の例では「いい天気だね」に対する応答メッセージは固定でしたが、mt_rand() メソッドで乱数を発生させ、複数の応答メッセージの中からランダムに応答させたりすれば、ずっと面白みがますでしょう。

 あなたの思うがままに My アクションを書き換えて、あなただけの HAL を作ってみて下さい。


ご購入

もう少々お待ち下さい。

体験版

version 1.5 から、IPアドレス制限が無くなり、ライセンス制になりました。ライセンスされていない場合、起動後30時間後に自動的に HAL を終了します。

体験版のインストーラー・スクリプトをダウンロード インストーラー・スクリプト
SHA-1: dd2a390b4f0f8c15301eaee23cd92bd5e831da91
※インストール方法については ソケットサーバー「HAL」の概要 (version 2.0対応版)~導入方法 を御覧ください。

体験版パッケージ
HAL_trial_2_0.tar.gz | version 2.0 | SHA-1: ce31802a5b423a9b0d73721a3d43e0585b009a6f
試用期限 2017/1/31 まで

この記事へのコメント

※現在コメントはMarkdown記法が強制です。>>Markdown の書き方


この記事に返信

このコメントに返信