Node-REDとRinnaAPIでAIお絵描きLINE Botを実装してみる

:::note info
`Node-RED`や`Node.js` + `express` でおうむ返しのLINE Botを作れる方が主に対象となります。参考記事は掲載していますが、`webhook URL`などわからないと難しいかもしれません
:::

## AIお絵描きが流行ったので私もやってみたい😍

そう思った方も多いかと思います。

https://page.line.me/877ieiqs

`stable diffusion`の発表からいち早く実装されたこのBotは凄まじい登録者数を獲得しました。LINE Botだったら自分にもできそう！！私も作ってみたい！！と思いstable diffusionのGitHubを訪ねてみると、待ち受けているのは残酷な現実です。

https://github.com/CompVis/stable-diffusion

**英語**
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2100048/4b89c841-72aa-612c-ce03-61b739c8defd.png" width=75%>

**`Python`環境構築**
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2100048/636c5dc5-b9f5-b448-b298-242400c628b5.png" width=75%>

ここで自分にはハードルが高いと感じ、諦めてしまった方もいらっしゃるのではないでしょうか。

本記事では`Node-RED`と`rinna Developers`を活用し、ノーコードでお絵描き`LINE Bot`の実装を目指します。

## 下準備

### Node-RED

https://zenn.dev/protoout/articles/59-nodered-railway-setup

この記事の通りRailway上にデプロイしておくと後々楽です。

### rinna Developers

https://developers.rinna.co.jp/product#product=text-to-image-api

こちら`Text to Image API`のサブスクリプションキーを発行しておいてください。適当に名前入力して`Subscribe`のボタンを押すだけです。

### rinna API

https://github.com/kazuhitoyokoi/node-red-contrib-rinna

こちらのNodeをお借りします。[Node-RED上にインストール](https://gyazo.com/d7badb17c3a1a44b47f7beabae1b44c3)しておいてください。

### LINE Developers

https://developers.line.biz/ja/

`Messaging API　`使うので登録しておいてください。

### gyazo API

https://gyazo.com/api?lang=ja

生成した画像をアップロードしてURLを生成、LINEに送ります。
参考記事はこちら。

https://qiita.com/n0bisuke/items/10f57597787588aa62a0

## Step.0 Node-RED上でテキストから画像生成してみる

再掲となりますが[こちら](https://github.com/kazuhitoyokoi/node-red-contrib-rinna)のGitHub上にあるサンプル通り、テキスト入力から画像を生成してみます。わかる方は飛ばしていただいて構いません。

<details><summary>詳細はこちら</summary>

1.画像の通りノードを並べてつなげます

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2100048/242044b6-38fc-563a-45f4-478cb3efae37.png" width=75%>

2.ノードの中身を設定します。（`inject`は送りたい文字列、`image previes`は設定不要です）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2100048/12dc0c74-1878-7816-72c0-6fc185741b4f.png" width=75%>

3.`inject`のボタンを押すと画像が生成されます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2100048/3e6f23eb-b629-2dea-a81a-29a329a17ea2.png" width=75%>

お好きな言葉で画像を生成することができます。

</details>

## Step.1 Node-REDの全体像

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2100048/097d718d-fb84-3a36-ea7d-8ff3b2fd981a.png" width=75%>

<details><summary>インポート用JSON配列</summary>

```json
[
    {
        "id": "679b23ea477d9d4c",
        "type": "rinna",
        "z": "736a148da489128d",
        "name": "",
        "api": "ldm",
        "subscription": "",
        "x": 330,
        "y": 400,
        "wires": [
            [
                "286ed77437845460",
                "f2a8012cd7145063",
                "24a5cb304229cf7c"
            ]
        ]
    },
    {
        "id": "286ed77437845460",
        "type": "image",
        "z": "736a148da489128d",
        "name": "",
        "width": 160,
        "data": "payload",
        "dataType": "msg",
        "thumbnail": false,
        "active": true,
        "pass": false,
        "outputs": 0,
        "x": 320,
        "y": 540,
        "wires": []
    },
    {
        "id": "a26acea9f7fe5dff",
        "type": "Webhook",
        "z": "736a148da489128d",
        "name": "",
        "url": "/webhook",
        "x": 160,
        "y": 400,
        "wires": [
            [
                "6c8d62d1fed760fb",
                "679b23ea477d9d4c"
            ]
        ]
    },
    {
        "id": "6c8d62d1fed760fb",
        "type": "debug",
        "z": "736a148da489128d",
        "name": "debug 5",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "statusVal": "",
        "statusType": "auto",
        "x": 160,
        "y": 480,
        "wires": []
    },
    {
        "id": "02eb29bdbc5713d2",
        "type": "ReplyMessage",
        "z": "736a148da489128d",
        "name": "",
        "replyMessage": "",
        "x": 760,
        "y": 760,
        "wires": []
    },
    {
        "id": "9b13730b6012b5b3",
        "type": "change",
        "z": "736a148da489128d",
        "name": "",
        "rules": [
            {
                "t": "set",
                "p": "headers.content-type",
                "pt": "msg",
                "to": "multipart/form-data",
                "tot": "str"
            },
            {
                "t": "set",
                "p": "temp",
                "pt": "msg",
                "to": "payload",
                "tot": "msg",
                "dc": true
            },
            {
                "t": "delete",
                "p": "payload",
                "pt": "msg"
            },
            {
                "t": "set",
                "p": "payload",
                "pt": "msg",
                "to": "{}",
                "tot": "json"
            },
            {
                "t": "set",
                "p": "payload.access_token",
                "pt": "msg",
                "to": "GYAZO_API_TOKEN",
                "tot": "str"
            },
            {
                "t": "set",
                "p": "payload.imagedata.value",
                "pt": "msg",
                "to": "temp",
                "tot": "msg",
                "dc": true
            },
            {
                "t": "set",
                "p": "payload.imagedata.options.filename",
                "pt": "msg",
                "to": "image.png",
                "tot": "str"
            },
            {
                "t": "delete",
                "p": "tmp",
                "pt": "msg"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 720,
        "y": 400,
        "wires": [
            [
                "01bbb139ce04e4ae",
                "523f4e49785e176d"
            ]
        ]
    },
    {
        "id": "f2a8012cd7145063",
        "type": "debug",
        "z": "736a148da489128d",
        "name": "debug 6",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "statusVal": "",
        "statusType": "auto",
        "x": 340,
        "y": 480,
        "wires": []
    },
    {
        "id": "efc81245f7303bb6",
        "type": "debug",
        "z": "736a148da489128d",
        "name": "debug 7",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "statusVal": "",
        "statusType": "auto",
        "x": 520,
        "y": 480,
        "wires": []
    },
    {
        "id": "24a5cb304229cf7c",
        "type": "function",
        "z": "736a148da489128d",
        "name": "function 4",
        "func": "const base64 = msg.payload.split(',')[1];\nmsg.payload = Buffer.from(base64, 'base64') //Buffer形式の画像バイナリに変換\n\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 500,
        "y": 400,
        "wires": [
            [
                "efc81245f7303bb6",
                "9b13730b6012b5b3"
            ]
        ]
    },
    {
        "id": "01bbb139ce04e4ae",
        "type": "debug",
        "z": "736a148da489128d",
        "name": "debug 8",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 700,
        "y": 480,
        "wires": []
    },
    {
        "id": "523f4e49785e176d",
        "type": "http request",
        "z": "736a148da489128d",
        "name": "",
        "method": "POST",
        "ret": "txt",
        "paytoqs": "ignore",
        "url": "https://upload.gyazo.com/api/upload",
        "tls": "",
        "persist": false,
        "proxy": "",
        "insecureHTTPParser": false,
        "authType": "basic",
        "senderr": false,
        "headers": [],
        "x": 150,
        "y": 760,
        "wires": [
            [
                "4d28f0dadd0a39b0",
                "8037b4c8.94c7c8"
            ]
        ]
    },
    {
        "id": "4d28f0dadd0a39b0",
        "type": "debug",
        "z": "736a148da489128d",
        "name": "debug 9",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 160,
        "y": 840,
        "wires": []
    },
    {
        "id": "cf90dab5.815908",
        "type": "debug",
        "z": "736a148da489128d",
        "name": "debug10",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 340,
        "y": 840,
        "wires": []
    },
    {
        "id": "8037b4c8.94c7c8",
        "type": "json",
        "z": "736a148da489128d",
        "name": "",
        "property": "payload",
        "action": "",
        "pretty": false,
        "x": 350,
        "y": 760,
        "wires": [
            [
                "cf90dab5.815908",
                "0adf342694d1eb65"
            ]
        ]
    },
    {
        "id": "0adf342694d1eb65",
        "type": "change",
        "z": "736a148da489128d",
        "name": "",
        "rules": [
            {
                "t": "set",
                "p": "temp",
                "pt": "msg",
                "to": "{}",
                "tot": "json"
            },
            {
                "t": "set",
                "p": "temp.thumb_url",
                "pt": "msg",
                "to": "payload.thumb_url",
                "tot": "msg",
                "dc": true
            },
            {
                "t": "set",
                "p": "temp.url",
                "pt": "msg",
                "to": "payload.url",
                "tot": "msg",
                "dc": true
            },
            {
                "t": "delete",
                "p": "payload",
                "pt": "msg"
            },
            {
                "t": "set",
                "p": "payload",
                "pt": "msg",
                "to": "{}",
                "tot": "json"
            },
            {
                "t": "set",
                "p": "payload.type",
                "pt": "msg",
                "to": "image",
                "tot": "str"
            },
            {
                "t": "set",
                "p": "payload.originalContentUrl",
                "pt": "msg",
                "to": "temp.url",
                "tot": "msg",
                "dc": true
            },
            {
                "t": "set",
                "p": "payload.previewImageUrl",
                "pt": "msg",
                "to": "temp.thumb_url",
                "tot": "msg",
                "dc": true
            },
            {
                "t": "delete",
                "p": "tmp",
                "pt": "msg"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 540,
        "y": 760,
        "wires": [
            [
                "a92b30d9b13897eb",
                "02eb29bdbc5713d2"
            ]
        ]
    },
    {
        "id": "a92b30d9b13897eb",
        "type": "debug",
        "z": "736a148da489128d",
        "name": "debug 11",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "statusVal": "",
        "statusType": "auto",
        "x": 540,
        "y": 840,
        "wires": []
    }
]

```

</details>

## Step.2 LINE側の設定をする

https://zenn.dev/ukkz/articles/a389105e400ea2

LINE側のwebhook URLは、`Railway`上にデプロイしているとして、
`https://node-red-railway-production-{自身のURL中文字列を入力}.up.railway.app/webhook`
Node-RED上のwebhook URLは`/webhook`で設定しておいてください。

## Step.3 各ノードを設定する

画像は折りたたんでいますので必要に応じて参照ください。

### 1. **LINE webhook**

Path: `/webhook`

<details><summary>詳細画像</summary>

![スクリーンショット 2022-11-06 22.00.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2100048/ca76502b-dead-ec22-4d1a-ecb5420b8f3e.png)

</details>

### 2. **rinna**

API: `Text to Image API`
subscrioption: `設定したsubscription key`

<details><summary>詳細画像</summary>

![スクリーンショット 2022-11-06 22.04.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2100048/a8cfd247-0df5-60c0-e9e3-f028b51f6373.png)

</details>

### 3. **function**

<details><summary>詳細画像</summary>

**ノーコードで実装するといったな。あれは嘘だ**

```js
const base64 = msg.payload.split(',')[1];
msg.payload = Buffer.from(base64, 'base64') //Buffer形式の画像バイナリに変換

return msg;
```
これをコピペするだけなので許してください。
`msg.payload = data:image/png;base64,{{base64}}`とうデータが入っているので、`data:image/png;base64,`を削除して、base64文字列を`Buffer`形式に変換できるノードがあればfunction使わずにすみます。

</details>

### 4. **change 1つめ**

https://qiita.com/ukkz/items/66fe09e7ae93ec91292d

この記事を参考に

```js
msg.headers.content-type = 'multipart/form-data'
msg.payload = {
  access_token: 'Gyazoアクセストークン文字列',
  imagedata: {
    value: (Buffer形式の画像バイナリ),
    options: {
      filename: 'image.png',
    },
  },
}
```
このようなmsgオブジェクトを生成してください。

<details><summary>インポート用JSON</summary>

```json
[
    {
        "id": "9b13730b6012b5b3",
        "type": "change",
        "z": "736a148da489128d",
        "name": "",
        "rules": [
            {
                "t": "set",
                "p": "headers.content-type",
                "pt": "msg",
                "to": "multipart/form-data",
                "tot": "str"
            },
            {
                "t": "set",
                "p": "temp",
                "pt": "msg",
                "to": "payload",
                "tot": "msg",
                "dc": true
            },
            {
                "t": "delete",
                "p": "payload",
                "pt": "msg"
            },
            {
                "t": "set",
                "p": "payload",
                "pt": "msg",
                "to": "{}",
                "tot": "json"
            },
            {
                "t": "set",
                "p": "payload.access_token",
                "pt": "msg",
                "to": "GYAZO_API_KEY",
                "tot": "str"
            },
            {
                "t": "set",
                "p": "payload.imagedata.value",
                "pt": "msg",
                "to": "temp",
                "tot": "msg",
                "dc": true
            },
            {
                "t": "set",
                "p": "payload.imagedata.options.filename",
                "pt": "msg",
                "to": "image.png",
                "tot": "str"
            },
            {
                "t": "delete",
                "p": "tmp",
                "pt": "msg"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 720,
        "y": 400,
        "wires": [
            [
                "01bbb139ce04e4ae",
                "523f4e49785e176d"
            ]
        ]
    }
]
```

</details>

### 5.　**http request**

メソッド: `POST`
URL: `https://upload.gyazo.com/api/upload`
認証を使用にチェックし、種別はBasic認証

<details><summary>詳細画像</summary>

![スクリーンショット 2022-11-06 22.22.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2100048/b43b90c9-e5dd-29aa-f28f-15900565d948.png)

</details>

### 6. **JSON**

responseをJSON形式にしてみやすくします。
別段設定は必要ありません。

<details><summary>詳細画像</summary>

![スクリーンショット 2022-11-06 22.25.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2100048/56633f44-809a-89af-2b8d-e292c4f2a25e.png)

</details>

### 7. **change 2つめ**

LINE Messaging APIリファレンスより

```json
{
  "type": "image",
  "originalContentUrl": "https://example.com/original.jpg",
  "previewImageUrl": "https://example.com/preview.jpg"
}
```
このようなJSONを生成します。
`originalContentURL`は詳細画像なので、GYAZO APIから返ってくる`url`の画像URLを、`previewImageURL`はサムネですので`thumb_url`の画像URLをそれぞれ渡してあげましょう。

<details><summary>インポート用JSON</summary>

```json
[
    {
        "id": "0adf342694d1eb65",
        "type": "change",
        "z": "736a148da489128d",
        "name": "",
        "rules": [
            {
                "t": "set",
                "p": "temp",
                "pt": "msg",
                "to": "{}",
                "tot": "json"
            },
            {
                "t": "set",
                "p": "temp.thumb_url",
                "pt": "msg",
                "to": "payload.thumb_url",
                "tot": "msg",
                "dc": true
            },
            {
                "t": "set",
                "p": "temp.url",
                "pt": "msg",
                "to": "payload.url",
                "tot": "msg",
                "dc": true
            },
            {
                "t": "delete",
                "p": "payload",
                "pt": "msg"
            },
            {
                "t": "set",
                "p": "payload",
                "pt": "msg",
                "to": "{}",
                "tot": "json"
            },
            {
                "t": "set",
                "p": "payload.type",
                "pt": "msg",
                "to": "image",
                "tot": "str"
            },
            {
                "t": "set",
                "p": "payload.originalContentUrl",
                "pt": "msg",
                "to": "temp.url",
                "tot": "msg",
                "dc": true
            },
            {
                "t": "set",
                "p": "payload.previewImageUrl",
                "pt": "msg",
                "to": "temp.thumb_url",
                "tot": "msg",
                "dc": true
            },
            {
                "t": "delete",
                "p": "tmp",
                "pt": "msg"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 540,
        "y": 760,
        "wires": [
            [
                "a92b30d9b13897eb",
                "02eb29bdbc5713d2"
            ]
        ]
    }
]
```

</details>


### 8. **LINE ReplyMessage**

LINE Developersからチャネルシークレット、チャネルアクセストークンをコピーして貼り付けましょう。

<details><sumary>詳細画像</summary>

![スクリーンショット 2022-11-06 22.33.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2100048/48cf301b-e0b0-1860-7f4e-2798acf1f444.png)

</details>

## Step.4 デプロイ

赤いボタンを押す

## 言葉から画像を生成してリプライしてもらった

https://twitter.com/shuyin02/status/1589259755369140224

:::note warn
編集で短くしていますが、メッセージ送った後それなりに待たされます。
:::

## 難点

<img src="https://i.gyazo.com/476b3961332edda82f1e2b22eb643aa8.png" width=50%>

やめてくれrinna。
待たされた後この画像出すのは俺に効く。