GoogleHomeアプリ「うんこカウンター」をNode-RED（IBMCloud）で作った

#はじめに
もう２年前ですが、GoogleHomeアプリ**「うんこカウンター」**を
Google Assistant＋Node-RED（IBMCloud）で作成しました。
https://assistant.google.com/services/a/uid/000000b4699e0c63?hl=ja-JP
![unko.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87274/a95da1c5-9517-dc98-6f20-f5835653f9a9.png)



この音声アプリはユーザーが喋った**「うんこ」**の数を**カウントするだけ**のアプリですが
当時リリースするや否や**爆発的アクセス**で、バックグラウンドのNode-REDが大丈夫か？
と危ぶまれたものですが、とりあえず現在もなんとか稼働してます。
**プログラミング初心者**が右も左もわからぬままコツコツ作ったアプリですので色々変なところ満載かと思いますが備忘録的に晒しておきます。

#紹介記事の紹介
**「うんこカウンター」**の遊び方とか色々紹介して頂いてます。マジ有難い記事ですので是非お読みください！
__GoogleHome：名前で判断してはいけない良作！__
__「う○こカウンター」・・・・・・伏せ字にしましたが「うんこ」です。__
https://sumasupi.net/2018/04/18/unko-counter/
#Actions on Googleの説明
この音声アプリのプロジェクトは**Actions on Google**と**Dialogflow**に作成いたします
詳しい設定方法の説明は省きますが以下の設定のスクショを参考にしてみて下さい
####Actions on Googleの設定
![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87274/2f6016b7-fa9f-7ff3-3f71-ee7454c379bf.png)

![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87274/1562aa35-05d4-0fe7-f6aa-b7b8b604b53a.png)

![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87274/9eaaec67-bb9d-33bd-2f44-d8cef0e50f55.png)
####Dialogflowの設定
![4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87274/674c3560-dc0b-d31e-0207-262956fb56cb.png)

![5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87274/6c01679f-4109-8425-c490-352aacaeec76.png)

![6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87274/a3870464-2ff4-63b9-d502-fd80ece8ba49.png)

![7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87274/40784556-e1ad-1bca-4181-a19290e54416.png)

![8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87274/8d9b3753-568b-e0d2-57c2-ad30a8a31277.png)

![9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87274/ef60bd61-6e90-0b45-26ea-5f52c0f293e0.png)

![10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87274/0f5363f2-094d-f390-28d6-2f871d90ed15.png)

![11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87274/dafe2eb5-13b6-79a2-2f23-f136c597a42e.png)

![12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87274/efb0be16-c326-3c0d-4ae9-9aa7e26e352a.png)


#アプリのバックグラウンド：Node-REDフロー
Google Assistantの音声認識出力(テキストデータ)をNode-RED(IBM Cloud)で処理しています
Node-REDでの基本的な処理は、**初回処理**、**通常処理**、**終了処理**の３通りです
GoogleHome/Assistantに向かってユーザーが「OK Google うんこカウンターにつないで」と喋りかけると**初回処理**に分岐されます。
それ以降は**通常処理**に分岐され「うんこ」の数をカウントし、効果音、音楽、カウント数に応じたコメントを返します。
最後にユーザーが「終わり」「やめて」などと喋ると**終了処理**に分岐され、最終コメントや効果音を返します。


![flow1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87274/3ed5e86b-922e-e9fa-9040-9a72f3ac8bd5.png)
Switchノードで処理を振り分けます
![switchノード.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87274/8763d826-282e-1ab7-54c5-26bf3fe717e9.png)

#効果音(SE)やBGM用のmp3ファイルを用意します。
ここが面倒と言えば面倒なのですが、音声アプリの醍醐味はSoundへの拘りだと思いますので頑張って下さい
GoogleAssistantで再生可能なファイルフォーマットは規定があります
通常の音楽用のHiFiなフォーマットとかだとNGです
https://developers.google.com/assistant/conversational/df-asdk/ssml#audio

また、GoogleAssistant用に予め用意された膨大なライブラリもありますので、こちらを利用できます
https://developers.google.com/assistant/tools/sound-library/cartoon

#mp3ファイルをデータベースにアップロード
自作したmp3ファイルはCloudantデータベースに格納します
![file_upload.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87274/60e82f53-2c4e-cf6b-7f79-3d48b319bd9c.png)
手作業で1個づつファイルをアップロードしてください
![file_upload2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87274/095c7bd5-f6bd-21fa-364b-0d2c9264c728.png)

#アップロードしたファイルの確認
アップロードしたファイルは以下の操作で確認/再生できますのでリンク先URLをメモしておいていて下さい
![file_check.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87274/8b4d3eb8-e4e2-58f7-fb0b-2b8a64ee6744.png)
#データベースのパーミションの設定
mp3ファイルを読み出せるようにパーミションを変更します
![permission.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87274/15b85225-9b48-336e-251b-57350170da2f.png)
#データベース（mp3）からのファイル読み出しを許可する
_readerにチェックを入れます
![permission2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87274/fc8be4b2-fb7c-266a-0e42-a377d6f5b960.png)

#初回処理
ユーザーの「OK Google うんこカウンターにつないで」で初回だけ実行される処理です
前回保存されたスコアを取得するようになっていますが、初回では無意味ですね
初回は操作方法の説明メッセージが必要です（公開リリース審査でチェックされます）

```javascript
//functionノード（初回処理）
//ユーザーストレージからユーザーのスコアを取得　無ければ生成
if(msg.payload.originalDetectIntentRequest.payload.user.userStorage){
    if(isNaN(msg.payload.originalDetectIntentRequest.payload.user.userStorage)){
        msg.userStorage = JSON.parse(msg.payload.originalDetectIntentRequest.payload.user.userStorage);
    }else{
        msg.userStorage = {"score":Number(msg.payload.originalDetectIntentRequest.payload.user.userStorage)};
    }
    
}else{
    msg.userStorage = {"score":0};
}
var text1 = "";
var text2 = "";
var url = 'https://xxxxxxxxxxxxxxxxxxxxxxx-bluemix.cloudant.com/mp3/SE/';
var unko_open = '<audio src="' + url + 'UNKO_OPEN.mp3"></audio>';
var beep1 = '<audio src="' + url + 'BEEP1.mp3"></audio>';
var music = '<audio src="https://xxxx.mybluemix.net/music.mp3"></audio>';

//---------------------　OPENING MASSAGE（0点のユーザー）-----------------------------
if ( msg.userStorage.score < 2 ){
    
    text1 ="うんこカウンターへようこそ！。。このアプリは、あなたが喋った文章を分析して、うんこの数をカウントします。。";
    text2 = "終わるときは、終わると言ってくださいね。。ビープ音が鳴ってから喋ってください。。それでは勇気を出して、スタート！";
    msg.payload = {
        "fulfillmentText": '<speak><prosody rate="fast" pitch="medium">' +
                            unko_open + text1 + text2 + beep1 + '</prosody></speak>',
        "payload" : {
            "google" : {
                "userStorage" : JSON.stringify(msg.userStorage)
                        }
                    }                    
    };
    msg.tweet = text1 + text2;
    return msg; 
}
//---------------------　OPENING MASSAGE（1点以上のユーザー）-----------------------------
if ( msg.userStorage.score ){
    text1 ="お帰りなさいませ！。。お待ちしておりました。。前回までの自己ベストは、";
    text2 ="点でしたっけ。。すみません忘れちゃいました。。それでは気合を入れて。スタート！";   
    msg.payload = {
        "fulfillmentText": '<speak><prosody rate="fast" pitch="medium">' +
            music + text1 + msg.userStorage.score + text2 + beep1 + '</prosody></speak>',
        "payload" : {
            "google" : {
                "userStorage" : JSON.stringify(msg.userStorage)
                        }
                    }   
    };
    msg.tweet = text1 + msg.userStorage.score + text2;
    return msg; 
}
```
#通常処理
前回で保存したスコアを取得します。「うんこ」の数をカウントして現在のスコアを超えたらスコアを更新し保存します
スコアに応じて色々コメントをしましょう！

```javascript
//functionノード（通常処理）
//ユーザーのスコアを取得
if(msg.payload.originalDetectIntentRequest.payload.user.userStorage){
    msg.userStorage = JSON.parse(msg.payload.originalDetectIntentRequest.payload.user.userStorage);
}else{
    msg.userStorage = {"score":0};
}

var text = msg.payload.queryResult.queryText
var text1 = ''
var text2 = ''
var text3 = ''
var sound1 = '<break time="1s"/>'
var url = 'https://xxxxxxxxxxxxxxxxxxxxxxxxxxxxx-bluemix.cloudant.com/mp3/SE/'
var beep1 = '<audio src="' + url + 'BEEP1.mp3"></audio>'
var wash = '<audio src="' + url + 'WASH.mp3"></audio>'
//var wash ='<audio src="https://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-bluemix.cloudant.com/mp3/music/LAST_MESSAGE.mp3"></audio>'



//---------------- UNKO COUNTER CENTER ------------------------
//はんこ　あんこ　わんこ　（漢字に変換されちゃうものは回避）
var unko = 0;
if(text.match(/うんこ/gm)){
    unko = text.match(/うんこ/gm).length;
}
if(text.match(/ウンコ/gm)){
    unko = unko + text.match(/ウンコ/gm).length;
}
if(text.match(/うんち/gm)){
    unko = unko + text.match(/うんち/gm).length;
}
if(text.match(/ウンチ/gm)){
    unko = unko + text.match(/ウンチ/gm).length;
}
if(text.match(/あんこ/gm)){
    unko = unko + text.match(/あんこ/gm).length;
}
if(text.match(/わんこ/gm)){
    unko = unko + text.match(/わんこ/gm).length;
}
if(text.match(/ハンコ/gm)){
    unko = unko + text.match(/ハンコ/gm).length;
}
if(text.match(/はんこ/gm)){
    unko = unko + text.match(/はんこ/gm).length;
}
//------------- NGワード -----------------
if(text.match(/ちんこ/gm)){
    unko = unko - text.match(/ちんこ/gm).length;
    text3 = "。NGワードで減点がありました。"
}
if(text.match(/まんこ/gm)){
    unko = unko - text.match(/まんこ/gm).length;
    text3 = "。NGワードでマイナスされてます。"
}

//-----------------自己ベスト更新-----------------------
if (msg.userStorage.score < unko){
    msg.userStorage.score = unko;
    text2 = 'おめでとうございます！自己ベスト更新です。';
    sound1 =  '<audio src="https://xxxx.mybluemix.net/music.mp3"></audio>'
}

//-----------------応援メッセージ-----------------------    
if (unko === 0){
    text = text + '。。うんこが検出されませんでした。';
    text3 = '頑張ってくださいね！';
} 
if (unko === 2){
    text2 =text2 + 'うん、この調子！';
} 
if (unko === 3){
    text2 =text2 + '頑張って下さいね。まだまだ行けます。';
}    
if (unko > 4){
    text = text + '。。。凄い！';
}
if (unko > 9){
    text = text + "。これは凄い！";
} 
if (unko > 14){
    text = text + "。かなり来てる！";
}
if (unko > 30){
    text = text + "。超やばい！";
} 
if (unko > 19){
    text2 ="。まだまだいけます。頑張って";
} 
if (unko > 24){
    text2 ="。しかしまさか、ココまで来るなんて！";
}
if (unko > 29){
    text2 ="。もう想定外ですので、応援コメントもここまで！";
}
if (unko > 34){
    text2 ="。想定外のさらに想定外！";
}
if (unko > 39){
    text2 ="。実は、まだ上の点数が狙えるらしい！";
}
if (unko > 49){
    text2 ="。そろそろスコアの限界が近いかも。フリーズとかするかも知れませんので覚悟して下さいね。";
}
if (unko > 59){
    text2 ="。ここまで来るのは不可能と思われていたのだが、よくぞ来ましたね！";
}
if (unko > 69){
    text2 ="。ここまで頑張ったあなたは神です。";
}
if (unko > 79){
    text2 ="。あと少しで完全制覇です。頑張って！！";
}
if (unko > 84){
    text2 ="。ついに完全制覇です。。。誠に申し訳ありませんが、これ以上はシステムが応答しなくなるとの報告がありましたので、ご了承ください。";
}
//---------------- SE音声データURL取得 -------------
var SE = flow.get("SE");
//---------------- RANDOM
var rnd = Math.round( Math.random()*30 );
msg.payload={
    "fulfillmentText": '<speak><prosody rate="fast" pitch="medium">' +
        beep1 + text +    //-----------------復唱
        '<audio src="'+ SE[rnd] + '"></audio>'+    //------効果音
        'カウント数は'+ unko +'点。。'+ sound1 + text2 +  text3 + beep1 + //次の入力開始合図のピッ！
        '</prosody></speak>',
    "payload" : {
        "google" : {
            "userStorage" : JSON.stringify(msg.userStorage)
                    }
                } 
    }
    msg.tweet = "カウント数は" + unko + "点。" + text2 + text3
return msg;
```
#終了処理
ユーザーの「おわり」「やめて」等でこの**終了処理**をします


```javascript
//functionノード（終了処理）
//ユーザースコア取得
msg.userStorage = JSON.parse(msg.payload.originalDetectIntentRequest.payload.user.userStorage)

var text = msg.payload.queryResult.queryText
var text1 = ''
var text2 = ''
var text3 = ''
var url = 'https://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-bluemix.cloudant.com/mp3/SE/'
var beep1 = '<audio src="' + url + 'BEEP1.mp3"></audio>'
var wash = '<audio src="' + url + 'WASH.mp3"></audio>'

//---------------------　ENDING MESSAGE 1　-----------------------------
if(msg.userStorage.score < 41 ){
    text1 ="どうもお疲れ様でした！。。今回の自己ベストは、";
    text2 ="点です。。次回また記録更新を狙いましょう。。それではまた！";    
    msg.payload = {
        "fulfillmentText" : '<speak><prosody rate="fast" pitch="medium">'+ text1 + msg.userStorage.score + text2 + wash + '</prosody></speak>',
        "payload" : {
            "google" : {
                "userStorage" : JSON.stringify(msg.userStorage)
                        }
                    }    
    }
    msg.tweet = text1 + msg.userStorage.score + text2
    return msg; 
}
//---------------------　ENDING MESSAGE 2　-----------------------------
if(msg.userStorage.score < 85 ){
    text1 ="いやはや、本当にお疲れ様でした！。。現在の自己ベストはなんと、";
    text2 ="点に到達いたしました。。この調子でがんばりましょう！。。。因みにリセットすれば最初からまた楽しめます。";    
    msg.payload = {
    "fulfillmentText" : '<speak><prosody rate="fast" pitch="medium">'+ text1 + msg.userStorage.score + text2 + wash + '</prosody></speak>',
    "payload" : {
        "google" : {
            "userStorage" : JSON.stringify(msg.userStorage)
                    }
                } 
            }
    msg.tweet = text1 + msg.userStorage.score + text2
    return msg; 
}
//---------------------　FINAL MESSAGE (スペシャルmp3再生でTTSはなし)-----------------------------
if(msg.userStorage.score > 84 ){
    text1 ="いやはや、本当にお疲れ様でした！。。ついに自己ベストが、";
    text2 ="点に、到達いたしました。。。思い起こせば、長く苛酷な戦いでしたね。。。そしてついに今、栄光のゴールを迎えました。。。この栄誉は永遠に語り継がれるでしょう。。。それでは、ごきげんよう。さようなら！";    
    msg.payload = {
    "fulfillmentText": '<speak><prosody rate="fast" pitch="medium">' +
        '<audio src="https://xxxxxxxxxxxxxxxxxxxxxxxxxxxxx-bluemix.cloudant.com/mp3/music/LAST_MESSAGE.mp3"></audio>' +
        '</prosody></speak>',
    "payload" : {
        "google" : {
            "userStorage" : JSON.stringify(msg.userStorage)
                    }
                }           
    }
    msg.tweet = text1 + msg.userStorage.score + text2
   return msg; 
}
```
#効果音のURL初期設定
お気に入りの効果音をサウンドライブラリの中から選んで、URLをここで纏めて設定しています
後で別の効果音に変更しても良いですね！

```javascript
//functionノード（デプロイ時初期化）
var SE =[
"https://actions.google.com/sounds/v1/sports/metal_bat_hits_baseball.ogg",	//野球ヒット
"https://actions.google.com/sounds/v1/animals/tentacle_flop_down.ogg",	//鈍い音
"https://actions.google.com/sounds/v1/cartoon/cartoon_boing.ogg",	//ぼいーん
"https://actions.google.com/sounds/v1/cartoon/cartoon_cowbell.ogg",	//Cartoon Cowbell
"https://actions.google.com/sounds/v1/cartoon/clang_and_wobble.ogg",	//Clang and wobble
"https://actions.google.com/sounds/v1/cartoon/cartoon_boing.ogg",	//Cartoon Boing
"https://actions.google.com/sounds/v1/cartoon/hair_ripping.ogg",	//Hair Ripping
"https://actions.google.com/sounds/v1/cartoon/instrument_strum.ogg",	//Instrument Strumギター
"https://actions.google.com/sounds/v1/cartoon/metal_twang.ogg",	//Metal Twang三味線
"https://actions.google.com/sounds/v1/impacts/stone_drop.ogg",	//カップイン
"https://actions.google.com/sounds/v1/cartoon/pop.ogg",	//ポン！
"https://actions.google.com/sounds/v1/sports/basketball_shot_rim_short.ogg",	//スポーツ
"https://actions.google.com/sounds/v1/cartoon/slap_with_glove.ogg",	//HandCrap
"https://actions.google.com/sounds/v1/impacts/stones_and_water_on_cement.ogg",	//セメント
"https://actions.google.com/sounds/v1/cartoon/slapping_three_faces.ogg",	//タタタ
"https://actions.google.com/sounds/v1/cartoon/wood_plank_flicks.ogg",	//木琴ドレミ
"https://actions.google.com/sounds/v1/emergency/emergency_radio_alert.ogg",	//ラジオDJ
"https://actions.google.com/sounds/v1/emergency/emergency_siren_short_burst.ogg",	//車アラーム
"https://actions.google.com/sounds/v1/impacts/dumpster_door_hit.ogg",	//鉄の扉
"https://actions.google.com/sounds/v1/impacts/crash_metal_sweetener_distant.ogg",	//発射
"https://actions.google.com/sounds/v1/impacts/debris_hits.ogg",	//ガラス破壊
"https://actions.google.com/sounds/v1/impacts/dumpster.ogg",	//金属叩く
"https://actions.google.com/sounds/v1/impacts/glass_drop_and_roll.ogg",	//空き瓶
"https://actions.google.com/sounds/v1/impacts/object_toss_and_smash.ogg",	//物音
"https://actions.google.com/sounds/v1/impacts/plastic_board_falling_over.ogg",	//物音
"https://actions.google.com/sounds/v1/impacts/fridge_door_fall_open.ogg",	//ドア破壊
"https://actions.google.com/sounds/v1/impacts/metal_crash.ogg",	//破壊
"https://actions.google.com/sounds/v1/impacts/small_glass_pane_shatter.ogg",	//ガラス飛び散り
"https://actions.google.com/sounds/v1/impacts/glass_windows_breaking.ogg",	//壁破壊
"https://actions.google.com/sounds/v1/impacts/glass_windows_crashing.ogg",	//破壊
]
flow.set("SE",SE)
msg.payload = SE
return msg;
```
####Actions on Googleのdeploy設定
Testで動作チェックして問題なければアプリを一般公開しましょう！
Deployの設定は以下
![13_deploy.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87274/ec563b85-6660-5204-b800-e075e237c072.png)

![14_deploy.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87274/bc8e2572-2ed7-0e9d-0f82-a6d88bb49142.png)

![15_deploy.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87274/69329297-1aa3-73f8-a566-69190f7a1a37.png)

![16_deploy.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87274/53de68e1-6610-2d8d-91d3-9cbfc9298840.png)

![17_deploy.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87274/eb7663d9-698f-b5dd-da90-f0d161db0698.png)

![18_deploy.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87274/d7642e6f-b022-7f81-5d22-760c69a5e60f.png)

![19_deploy.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87274/ba8224a7-ab66-cc92-3f01-f78f03265499.png)

![20_deploy.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87274/03e9536e-46da-c746-8328-cb4a745cb40a.png)

![21_deploy.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87274/6f82c3c4-9275-c12e-c86f-d56434f1e33c.png)

#まとめ
説明不足いっぱいな感じで大変申し訳ないですがとりあえず公開させて頂きました
随時更新したいと思います（更新2021/09/16）