Teachable Machineを使って、ゆでたまごの気持ちをツンデレ風に表現してみた

ゆで具合によって、気持ちを表現してくれます。
ちなみに、<font color="red">**気まぐれなゆで卵はツンデレ感満載です。というか基本やさぐれています。**</font>
**ダイエットには咀嚼が多い方が満腹感も得られるので、ちょっと固めのゆで卵が作れると褒めてくれます。柔らか過ぎても固過ぎても怒られます。**

<img width="600" alt="ゆでたまごの気持ち" src="https://user-images.githubusercontent.com/90755760/139859438-1db68f4a-7bb6-47fc-bb54-5e8eb5886054.gif">

**ゆで卵って茹でるだけだけなのに、なんだか難しくないですか？**
水から？沸騰してから？冷蔵庫から出したて？常温？いつもと同じ時間茹でているのに、なぜか毎回仕上がりに。卵ってやつは本当に気まぐれだなーっていつも思います。。。

**趣味はダイエット、特技はリバウンド**のは私はよくゆで卵を作り空腹をしのぎます。
ゆで卵を毎回作るのは面倒くさくなり、結局長続きせず。
少しでもゆで卵作りが楽しくなってダイエットが続けば良いと思い、Teachable Machineを使ってゆで卵の気持ちを表現してみました。

:::note warn
・茹で時間5分：5分茹でたの？あと4分茹でないとあなたの好みの固さにはならないよ。
・茹で時間6分：6分くらい茹でた？あと3分茹でてちょうだい。
・茹で時間7分：7分茹でた？いい感じだけど、もう少し固めに茹でなさい。
・茹で時間8分：8分かな？とても惜しいわ。もう一歩よ。
★茹で時間9分：9分！やればできるじゃない！あなたの好みに仕上がっているわ。
・茹で時間10分：10分？ちょっと茹で過ぎね。でも許容範囲だから許してあげる。
・茹で時間11分：11分？茹で過ぎ。あなた私を扱うセンスないね。
・茹で時間12分：12分？話にならない。こんな扱いするなら二度と茹でないで。
:::

##Teachable Machine
[Teachable Machine](https://teachablemachine.withgoogle.com/)の画像プロジェクトを今回は使用しました。
>Teachable Machine は、誰でも短時間で簡単に機械学習モデルを作成できる、ウェブベースのツールです。

https://teachablemachine.withgoogle.com/

###学習させるにあたり、撮影工夫ポイント
- 1分毎に茹で時間を変えた茹で卵を用意（5～12分の8パターン） 
<img width="500" alt="たまご" src="https://user-images.githubusercontent.com/90755760/139583541-2bddb085-7eec-4cea-addf-49fc0fe2a2f8.jpg">
<img width="500" alt="ゆでたまご" src="https://user-images.githubusercontent.com/90755760/139855835-8f8cf281-f1dc-42ff-a747-4bd54cab6706.jpg">
- サンプルは角度を変えて約50パターン撮影
- 引き・アップも約50パターン撮影
- 個数を変えて約50パターン撮影
- 特に黄身の色の変化が画像として分かりやすいと思い、重点的に約50パターン撮影
- 職場でも撮影できるように、デスクと同じ色の白いテーブルで撮影

※ゆで卵は撮影後、必死に美味しくいただきました

###しっかり画像認識できているかの確認###
<img width="300" alt="ゆでたまごテスト" src="https://user-images.githubusercontent.com/90755760/139857498-4d73fa41-f62b-4971-9517-c8dce64bcf87.gif">

**ちょっとあやしいけど、大丈夫そうだ！**

##CodePen
次は[CodePen](https://codepen.io/trending)で表現します。

>CodePenはWebブラウザ上でHTML/CSS/JavaScriptなど、主にフロントエンド言語のコーディングができるサービスです。CodePenを利用する目的としてもっとも多いのが、ソースコードをブログやWebサイトなどでシェアすること。コーディングしたものがリアルタイムで反映されるため、コーディング初心者の練習用にも使えます。

<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="qBXPrrL" data-user="tanakahiroki1" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/tanakahiroki1/pen/qBXPrrL">
  ゆでたまご</a> by 田中大樹 (<a href="https://codepen.io/tanakahiroki1">@tanakahiroki1</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

```
body{background-color: yellow;  
```
を入れることにより、たまごの黄身感を表現しています。
また、**黄色は精神的刺激を求める色**と言われ、**ツンデレ感を密かに演出**しています。

<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="PoKOdgP" data-user="tanakahiroki1" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/tanakahiroki1/pen/PoKOdgP">
  Canvasで「ざわ・・・ざわ・・・」</a> by 田中大樹 (<a href="https://codepen.io/tanakahiroki1">@tanakahiroki1</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>
実際使うと**今日は怒られるのかな？褒められるかな？**と意外と緊張します笑
そんな時にざわざわメーカーとやらを発見しました！
他の方が書いたものをすぐに使用できるのは凄く助かりますし、いい文化ですよね！
肝心の出来はというと、上のシンプルな方にコードをドッキングしたところ、カメラはちゃんと起動しますが**ゆで卵の気持ちの言葉が表示されず、ここはまだ未解決のままです。**

##Teachable MachineとLINE Botを組み合わせてみる
会社にゆで卵を持っていくことも多く、使いやすいようにTeachable MachineとLINE Botを組み合わせてみました。
<iframe width="560" height="315" src="https://www.youtube.com/embed/fkH08OA7C6E" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

参考：[憧れのギニュー特戦隊の誰に似てるか判定するLINEbotを作ったから、ぜってぇ見てくれよなっ！](https://qiita.com/Teru_3/items/109517e6a6e3b761285d)
参考：[1時間でLINE BOTを作るハンズオン (資料+レポート) in Node学園祭2017 #nodefest](https://qiita.com/n0bisuke/items/ceaa09ef8898bee8369d)
**※どちらの記事もわかりやすく丁寧に書かれているので、見れば開発環境設定から、code、トンネリングまでわかります！**

##環境
Visual Studio Code v1.60.2
Node.js v16.10.0
npm v7.21.1
ngrok 2.3.40

##コード
※LINEbotのチャネルシークレットとチャネルアクセストークンを忘れず変更
<details><summary>**コード（クリックで表示）**</summary><div>

```js
'use strict'; 

const { JSDOM } = require('jsdom');
var dom = new JSDOM('');
global.document = dom.window.document;
global.HTMLVideoElement = dom.window.HTMLVideoElement;
const canvas = require('canvas');
global.fetch = require('node-fetch');
const tmImage = require('@teachablemachine/image');

const express = require('express');
const line = require('@line/bot-sdk');
const fs = require('fs');

const PORT = process.env.PORT || 3000;

const config = {
    channelSecret: 'チャネルシークレット', //忘れず変更
    channelAccessToken: 'チャネルアクセストークン', //忘れず変更
};

// Teachable Machine
let model;

// https://teachablemachine.withgoogle.com/
// ここでエクスポート、クラウドにモデルをアップロードした後に取得できる
const URL = 'https://teachablemachine.withgoogle.com/models/ucDlkeBZQ/';

// ########################################
//  Teachable Machineを使って画像分類をする部分
// ########################################

// 初期化が時間かかるので、node立ち上げ時に行うようにする
async function initTeachableMachine() {
  const modelURL = URL + 'model.json';
  const metadataURL = URL + 'metadata.json';
  // モデルデータのロード
  model = await tmImage.load(modelURL, metadataURL);

  // クラスのリストを取得
  // const classes = model.getClassLabels();
  // console.log(classes);
}
initTeachableMachine();

async function predict(imgPath) {
  // canvasに画像をロードする
  const image = await canvas.loadImage(imgPath);

  // 判定する
  const predictions = await model.predict(image);

  // 一番近いもの順でソート
  predictions.sort((a, b) => {
    return b.probability - a.probability;
  });

  return predictions;
}

// ########################################
//  LINEサーバーからのWebhookデータを処理する部分
// ########################################

// LINE SDKを初期化します
const client = new line.Client(config);

// LINEサーバーからWebhookがあると「サーバー部分」から以下の "handleEvent" という関数が呼び出されます
async function handleEvent(event) {
  // 受信したWebhookが「画像以外」であればnullを返すことで無視します
  if (event.message.type === 'image') {
    console.log("画像が送られてきた");

    // 画像を保存
    const downloadPath = './01.png'; //左記の名前で使用中のフォルダ内に送信された画像が保存される
    const getContent = await downloadContent(event.message.id, downloadPath);

    const result = await predict(downloadPath);

    // AIメーカーAPIの結果から、返信するメッセージを組み立てる
    let text = '';
    let name = '';
    name = result[0].className
    // 判定結果をテキストに代入
    text = name;
    // これまでの結果を確認するためにコンソールに表示
    console.log(result);
    console.log(name);
    console.log(text);
    // 判定結果に応じた画像を送信
    if(name === '5分茹でたの？あと4分茹でないとあなたの好みの固さにはならないよ。'){
        return client.replyMessage(event.replyToken, [{
            type: 'text',
            text: text
        }]);
     }else if(name === '6分くらい茹でた？あと3分茹でてちょうだい。'){
        return client.replyMessage(event.replyToken,[{
            type: 'text',
            text: text
        }]);
     }else if(name === '7分茹でた？いい感じだけど、もう少し固めに茹でなさい。'){
        return client.replyMessage(event.replyToken, [{
            type: 'text',
            text: text
        }]);
     }else if(name === '8分かな？とても惜しいわ。もう一歩よ。'){
        return client.replyMessage(event.replyToken, [{
            type: 'text',
            text: text
        }]);
     }else if(name === '9分！やればできるじゃない！あなたの好みに仕上がっているわ。'){
        return client.replyMessage(event.replyToken, [{
            type: 'text',
            text: text
        }]);
    }else if(name === '10分？ちょっと茹で過ぎね。でも許容範囲だから許してあげる。'){
        return client.replyMessage(event.replyToken, [{
            type: 'text',
            text: text
        }]);
     }else if(name === '11分？茹で過ぎ。あなた私を扱うセンスないね。'){
        return client.replyMessage(event.replyToken, [{
            type: 'text',
            text: text
        }]);
     }else if(name === '12分？話にならない。こんな扱いするなら二度と茹でないで。'){
        return client.replyMessage(event.replyToken, [{
            type: 'text',
            text: text
        }]);
    }
  }

  // 「テキストメッセージ」であれば、受信したテキストをそのまま返事します
  if (event.message.type === 'text') {
    return client.replyMessage(event.replyToken, {
      type: 'text',
      text: event.message.text // ← ここに入れた言葉が実際に返信されます
    });
  }
}

// ########################################
//          LINEで送られた画像を保存する部分
// ########################################

function downloadContent(messageId, downloadPath) {
  const data = [];
  return client.getMessageContent(messageId)
    .then((stream) => new Promise((resolve, reject) => {
      const writable = fs.createWriteStream(downloadPath);
      stream.on('data', (chunk) => data.push(Buffer.from(chunk)));
      stream.pipe(writable);
      stream.on('end', () => resolve(Buffer.concat(data)));
      stream.on('error', reject);
    }));
}

// ########################################
//          Expressによるサーバー部分
// ########################################

// expressを初期化します
const app = express();

// HTTP GETによって '/' のパスにアクセスがあったときに 'Hello LINE BOT! (HTTP GET)' と返事します
// これはMessaging APIとは関係のない確認用のものです
app.get('/', (req, res) => res.send('<h1>Hello LINE BOT! (HTTP GET)</h1>'));

// HTTP POSTによって '/webhook' のパスにアクセスがあったら、POSTされた内容に応じて様々な処理をします
app.post('/webhook', line.middleware(config), (req, res) => {
  // Webhookの中身を確認用にターミナルに表示します
  console.log(req.body.events);

  // 空っぽの場合、検証ボタンをクリックしたときに飛んできた"接続確認"用
  // 削除しても問題ありません
  if (req.body.events.length == 0) {
    res.send('Hello LINE BOT! (HTTP POST)'); // LINEサーバーに返答します
    console.log('検証イベントを受信しました！'); // ターミナルに表示します
    return; // これより下は実行されません
  }

  // あらかじめ宣言しておいた "handleEvent" 関数にWebhookの中身を渡して処理してもらい、
  // 関数から戻ってきたデータをそのままLINEサーバーに「レスポンス」として返します
  Promise.all(req.body.events.map(handleEvent)).then((result) => res.json(result));
});

// 最初に決めたポート番号でサーバーをPC内だけに公開します
// （環境によってはローカルネットワーク内にも公開されます）
app.listen(PORT);
console.log(`ポート${PORT}番でExpressサーバーを実行中です…`);
```

</div></details>




<details><summary>**package.jsonの"scripts"の中に"start": "node ファイル名"を入力（クリックで表示）**</summary><div>

```js
{
  "name": "egg-bot",
  "version": "1.0.0",
  "description": "",
  "main": "egg.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node test3.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@line/bot-sdk": "^7.4.0",
    "@teachablemachine/image": "^0.8.5",
    "@teachablemachine/pose": "^0.8.6",
    "@tensorflow/tfjs": "^1.3.1",
    "canvas": "^2.8.0",
    "express": "^4.17.1",
    "jimp": "^0.16.1",
    "jsdom": "^18.0.1"
  }
}
```

</div></details>


<details><summary>**つまづいたエラー①（クリックで表示）**</summary><div>

```js
npm i @teachablemachine/image @tensorflow/tfjs canvas jsdom　jimp
```

をインストールしなければならないところを

```js
npm i @teachablemachine/pose　@tensorflow/tfjs canvas jsdom　jimp
```

をインストールしていてエラー出現　→　正しいものをインストールし直して解決
エラー内容tensorflowあたりがTypeErrorを起こしてnot a function的な感じです。

```.js
TypeError: forwardFunc is not a function
    at C:\Users\user\Desktop\egg-bot\node_modules\@tensorflow\tfjs\node_modules\@tensorflow\tfjs-core\dist\engine.js:524:55
    at C:\Users\user\Desktop\egg-bot\node_modules\@tensorflow\tfjs\node_modules\@tensorflow\tfjs-core\dist\engine.js:387:22
    at Engine.scopedRun (C:\Users\user\Desktop\egg-bot\node_modules\@tensorflow\tfjs\node_modules\@tensorflow\tfjs-core\dist\engine.js:397:23)
    at Engine.tidy (C:\Users\user\Desktop\egg-bot\node_modules\@tensorflow\tfjs\node_modules\@tensorflow\tfjs-core\dist\engine.js:386:21)
    at kernelFunc (C:\Users\user\Desktop\egg-bot\node_modules\@tensorflow\tfjs\node_modules\@tensorflow\tfjs-core\dist\engine.js:524:29)
    at C:\Users\user\Desktop\egg-bot\node_modules\@tensorflow\tfjs\node_modules\@tensorflow\tfjs-core\dist\engine.js:535:27
    at Engine.scopedRun (C:\Users\user\Desktop\egg-bot\node_modules\@tensorflow\tfjs\node_modules\@tensorflow\tfjs-core\dist\engine.js:397:23)
    at Engine.runKernelFunc (C:\Users\user\Desktop\egg-bot\node_modules\@tensorflow\tfjs\node_modules\@tensorflow\tfjs-core\dist\engine.js:533:14)
    at Engine.runKernel (C:\Users\user\Desktop\egg-bot\node_modules\@tensorflow\tfjs\node_modules\@tensorflow\tfjs-core\dist\engine.js:448:21)
    at pad_ (C:\Users\user\Desktop\egg-bot\node_modules\@tensorflow\tfjs-core\dist\tf-core.node.js:16791:19)
```

</div></details>



<details><summary>**つまづいたエラー②（クリックで表示）**</summary><div>

参考にしたコード一部が

```js
tmPose
```

になっていて、

```js
tmImage
```

に変更したところ、エラーは解消

エラー内容はtmImageが定義出来ていないからエラーが出た感じです。

```.js
ポート3000番でExpressサーバーを実行中です…
C:\Users\user\Desktop\egg-bot\egg.js:40
  model = await tmImage.load(modelURL, metadataURL);
  ^

ReferenceError: tmImage is not defined
    at initTeachableMachine (C:\Users\user\Desktop\egg-bot\egg.js:40:3)
    at Object.<anonymous> (C:\Users\user\Desktop\egg-bot\egg.js:47:1)
    at Module._compile (node:internal/modules/cjs/loader:1101:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1153:10)
    at Module.load (node:internal/modules/cjs/loader:981:32)
    at Function.Module._load (node:internal/modules/cjs/loader:822:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:79:12)
    at node:internal/main/run_main_module:17:47
```

</div></details>

##会社の同僚に使ってもらった
作りたてほやほやのツンデレゆでたまごをさっそく同僚にお披露目しました！
ダイエット・リバウンド仲間の女子社員に使ってもらったところ、

<font color="orange">**怒られるのは嫌です！笑
だけど、ダイエットしている時って結局自分との戦いで寂しくもなるから、
ちょっと笑えるこういうのがあるとテンション上がりますね！
私の場合は、山﨑賢人さんに「ゆで卵作るのうまいね！」「僕も食べたい！」とか言って欲しいです！
写真とか出てきたら尚更ダイエット頑張れる気がします！！
でも写真が勝手に出てくるとか無理ですよね？笑**</font>

だそうです。

いや、俺も別にツンデレさんに怒られたいと思っているわけではないからと必死に弁解しました。

ヒアリングして感想をもらって、「たしかに画像もあればいいな」と。
同時に、**テキスト以外に画像リプライくらいならLINE Botで俺でも出来るかも**と感じました。
プログラミングを習い始めて1か月経ちましたが、1か月前の自分ではまず感じないことだと思い、
<font color="blue">**習うこと・知ることで、物事を捉える解像度が上がって、出来ることが増えている**</font>ということを実感しました。
1か月後の自分が楽しみです！

最後まで読んでいただき、ありがとうございました！