Node-REDでCoeFont CLOUD APIを使い高品質のTTSを試す

# はじめに

以前、スマートスピーカーやTTSの音声が聞き取りづらかった経緯があり、「[あみたろの声素材工房](https://amitaro.net/)」から音声素材を頂いて、Node-REDで時報などを発話させていました。
そして今回、あみたろさんが[CoeFont](https://coefont.cloud/)というAI音声合成で声のフォントを作成されたので、今まで音声素材の組み合わせでは実現できなかったセリフを作って活用しようと考えています。

https://twitter.com/amitaro_voice/status/1420281254704914433?s=21

そして、CoeFont CLOUDでは[CoeFont API](https://docs.coefont.cloud/#tag/Text2speech/paths/~1text2speech/post)が用意されており、TTS(Text-to-Speach)で利用できるようです。
CoeFontは実に色々な方の色々な表情の声が利用できるので、今までのTTSでは難しかった以下のような発話ができます。

- 滑らかで自然な発話になる！
- 自分の好みの声を選べる！(その声の人を応援できる！)
- 感情も表現できたりする！

# 前提環境
サブフローのテンプレートで環境変数が扱えノードのようにできるバージョン以上を前提としています。

- Node-RED v1.3以降
- CoeFontアカウント作成後、[Access KeyとClient secretの発行](https://coefont.cloud/account/api)

# Node-REDサブフローの作成
CoeFont APIドキュメントにnode.jsのソースがあるので、ほぼそのまま利用します。

https://docs.coefont.cloud/#tag/Text2speech

事前に、request,cryptoのモジュールをNode-REDのホームディレクトリ(ここでは~/.node-redとして説明します)でインストールしておきます。

```
cd ~/.node-red
npm install axios crypto
```

~/.node-red/settings.js の functionGlobalContext に request,cryptoのモジュールを定義して、functionノードで利用できるようにしておきます。

```yaml:settings.js
           : (略)
    functionGlobalContext: {
        os: require('os'),
        axios: require('axios'),
        crypto: require('crypto')
    },
           : (略)
```

もしくは、v1.3の新機能 [externalModules](https://nodered.jp/blog/2021/04/08/version-1-3-released#function-node-use-of-npm-modules) を利用してください。※個人的に使った事はまだありません。

https://nodered.jp/blog/2021/04/08/version-1-3-released#function-node-use-of-npm-modules

Node-REDを再起動して、以下のフローを読み込むと「CoeFont API」がサブフローとして使えるようになります。

↓三角をクリックでJSONデータが展開されます。
<details><summary>CoeFont CLOUD サブフロー</summary><div>

```json:
[
  {
    "id": "adcdb07133d05fd7",
    "type": "subflow",
    "name": "CoeFont CLOUD",
    "info": "",
    "category": "",
    "in": [
      {
        "x": 50,
        "y": 80,
        "wires": [
          {
            "id": "7cb8f439e55c1230"
          }
        ]
      }
    ],
    "out": [
      {
        "x": 410,
        "y": 80,
        "wires": [
          {
            "id": "c714d2a12696eeba",
            "port": 0
          }
        ]
      }
    ],
    "env": [
      {
        "name": "KEY",
        "type": "str",
        "value": "",
        "ui": {
          "icon": "font-awesome/fa-key",
          "label": {
            "en-US": "Access Key"
          },
          "type": "input",
          "opts": {
            "types": [
              "str"
            ]
          }
        }
      },
      {
        "name": "SECRET",
        "type": "cred",
        "ui": {
          "icon": "font-awesome/fa-user-secret",
          "label": {
            "en-US": "Client Secret"
          }
        }
      },
      {
        "name": "COEFONT",
        "type": "str",
        "value": "33240884-bd28-46c0-820e-f30b16290e08",
        "ui": {
          "icon": "font-awesome/fa-address-book-o",
          "label": {
            "en-US": "coefont ID"
          },
          "type": "select",
          "opts": {
            "opts": [
              {
                "l": {
                  "en-US": "あみたろ・ハキハキ"
                },
                "v": "33240884-bd28-46c0-820e-f30b16290e08"
              }
            ]
          }
        }
      },
      {
        "name": "SPEED",
        "type": "num",
        "value": "1.0",
        "ui": {
          "icon": "font-awesome/fa-forward",
          "label": {
            "en-US": "speed"
          },
          "type": "spinner",
          "opts": {
            "min": 0,
            "max": 10
          }
        }
      },
      {
        "name": "PITCH",
        "type": "num",
        "value": "0",
        "ui": {
          "icon": "font-awesome/fa-sort",
          "label": {
            "en-US": "pitch (x10)"
          },
          "type": "spinner",
          "opts": {
            "min": -300,
            "max": 300
          }
        }
      },
      {
        "name": "KUTEN",
        "type": "num",
        "value": "0.7",
        "ui": {
          "icon": "font-awesome/fa-spinner",
          "label": {
            "en-US": "kuten"
          },
          "type": "input",
          "opts": {
            "types": [
              "num"
            ]
          }
        }
      },
      {
        "name": "TOTEN",
        "type": "num",
        "value": "0.4",
        "ui": {
          "icon": "font-awesome/fa-spinner",
          "label": {
            "en-US": "toten"
          },
          "type": "input",
          "opts": {
            "types": [
              "num"
            ]
          }
        }
      },
      {
        "name": "VOLUME",
        "type": "num",
        "value": "1.0",
        "ui": {
          "icon": "font-awesome/fa-volume-up",
          "label": {
            "en-US": "volume"
          },
          "type": "spinner",
          "opts": {
            "min": 0,
            "max": 5
          }
        }
      },
      {
        "name": "INTONATION",
        "type": "num",
        "value": "1.0",
        "ui": {
          "icon": "font-awesome/fa-music",
          "label": {
            "en-US": "intonation"
          },
          "type": "spinner",
          "opts": {
            "min": 0,
            "max": 2
          }
        }
      },
      {
        "name": "URI",
        "type": "str",
        "value": "https://api.coefont.cloud/v1/text2speech",
        "ui": {
          "icon": "font-awesome/fa-globe",
          "type": "input",
          "opts": {
            "types": [
              "str"
            ]
          }
        }
      },
      {
        "name": "APIドキュメント：https://docs.coefont.cloud/",
        "type": "str",
        "value": "",
        "ui": {
          "type": "none"
        }
      }
    ],
    "meta": {},
    "credentials": {
      "SECRET": ""
    },
    "color": "#3FADB5",
    "icon": "font-awesome/fa-commenting"
  },
  {
    "id": "c714d2a12696eeba",
    "type": "switch",
    "z": "adcdb07133d05fd7",
    "name": "",
    "property": "payload",
    "propertyType": "msg",
    "rules": [
      {
        "t": "istype",
        "v": "buffer",
        "vt": "buffer"
      }
    ],
    "checkall": "false",
    "repair": false,
    "outputs": 1,
    "x": 300,
    "y": 80,
    "wires": [
      []
    ]
  },
  {
    "id": "7cb8f439e55c1230",
    "type": "function",
    "z": "adcdb07133d05fd7",
    "name": "",
    "func": "\nconst axios = new global.get('axios');\nconst crypto = new global.get('crypto');\n\nconst data = JSON.stringify({\n  'coefont': env.get(\"COEFONT\"),\n  'text': msg.payload,\n  'speed': env.get(\"SPEED\"),\n  'pitch': env.get(\"PITCH\") * 10,\n  'kuten': env.get(\"KUTEN\"),\n  'toten': env.get(\"TOTEN\"),\n  'volume': env.get(\"VOLUME\"),\n  'intonation': env.get(\"INTONATION\")\n});\nconst date = String(Math.floor(Date.now() / 1000));\n\nconst signature = crypto.createHmac('sha256', env.get(\"SECRET\")).update(date+data).digest('hex')\n\naxios.post(env.get(\"URI\"), data, {\n    headers: {\n      'Content-Type': 'application/json',\n      'Authorization': env.get(\"KEY\"),\n      'X-Coefont-Date': date,\n      'X-Coefont-Content': signature\n    },\n    responseType: 'arraybuffer'\n  })\n  .then(response => {\n    msg.payload = new Buffer(response.data, \"binary\");\n  })\n  .catch(error => {\n    console.log(error)\n  })\n  .finally(() => {\n    node.send(msg);\n  })\n\nreturn null;",
    "outputs": 1,
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 160,
    "y": 80,
    "wires": [
      [
        "c714d2a12696eeba"
      ]
    ]
  }
]
```

</div></details>

入力は、喋らせたい内容を msg.payload に設定しておきます。
出力は、wav形式のファイルがバイナリバッファで msg.payload に保存されています。

サブフローの設定項目は以下のようになっています。
調整値の詳細については、[CoeFont APIドキュメント](https://docs.coefont.cloud/#tag/Text2speech/paths/~1text2speech/post)を参照してください。

| 項目 | 説明 |
|:-----------:|:------------:|
| Access Key | CoeFont CLOUDで発行したAccess Keyを指定します。 |
| Client Secret | CoeFont CLOUDで発行したClient Secretを指定します。 |
| coefont ID | CoeFont ID(UUID)を指定します。 |
| speed | 音声の速度(倍)を指定します。 |
| pitch (x10) | 音声のピッチを指定します。 |
| kuten | 句点間隔(秒)を指定します。 |
| toten | 読点間隔(秒)を指定します。 |
| volume | 音量(倍)を指定します。 |
| intonation | 抑揚を指定します。 |
| URI | リクエストURIを設定します。 |

# 試してみる
事前に、「[node-red-contrib-play-audio](https://flows.nodered.org/node/node-red-contrib-play-audio)」ノードをインストールしておきます。
CoeFontサブフローに続けて「play audio」ノードでブラウザから喋らせています。

![スクリーンショット 2021-09-09 22.47.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/5fb834c9-a1fb-589d-fd35-6d2b27518667.png)

↓三角をクリックでこのフローのJSONデータが展開されます。
<details><summary>サンプルフロー</summary><div>

```json:
[
  {
    "id": "adcdb07133d05fd7",
    "type": "subflow",
    "name": "CoeFont CLOUD",
    "info": "",
    "category": "",
    "in": [
      {
        "x": 50,
        "y": 80,
        "wires": [
          {
            "id": "7cb8f439e55c1230"
          }
        ]
      }
    ],
    "out": [
      {
        "x": 410,
        "y": 80,
        "wires": [
          {
            "id": "c714d2a12696eeba",
            "port": 0
          }
        ]
      }
    ],
    "env": [
      {
        "name": "KEY",
        "type": "str",
        "value": "",
        "ui": {
          "icon": "font-awesome/fa-key",
          "label": {
            "en-US": "Access Key"
          },
          "type": "input",
          "opts": {
            "types": [
              "str"
            ]
          }
        }
      },
      {
        "name": "SECRET",
        "type": "cred",
        "ui": {
          "icon": "font-awesome/fa-user-secret",
          "label": {
            "en-US": "Client Secret"
          }
        }
      },
      {
        "name": "COEFONT",
        "type": "str",
        "value": "33240884-bd28-46c0-820e-f30b16290e08",
        "ui": {
          "icon": "font-awesome/fa-address-book-o",
          "label": {
            "en-US": "coefont ID"
          },
          "type": "select",
          "opts": {
            "opts": [
              {
                "l": {
                  "en-US": "あみたろ・ハキハキ"
                },
                "v": "33240884-bd28-46c0-820e-f30b16290e08"
              }
            ]
          }
        }
      },
      {
        "name": "SPEED",
        "type": "num",
        "value": "1.0",
        "ui": {
          "icon": "font-awesome/fa-forward",
          "label": {
            "en-US": "speed"
          },
          "type": "spinner",
          "opts": {
            "min": 0,
            "max": 10
          }
        }
      },
      {
        "name": "PITCH",
        "type": "num",
        "value": "0",
        "ui": {
          "icon": "font-awesome/fa-sort",
          "label": {
            "en-US": "pitch (x10)"
          },
          "type": "spinner",
          "opts": {
            "min": -300,
            "max": 300
          }
        }
      },
      {
        "name": "KUTEN",
        "type": "num",
        "value": "0.7",
        "ui": {
          "icon": "font-awesome/fa-spinner",
          "label": {
            "en-US": "kuten"
          },
          "type": "input",
          "opts": {
            "types": [
              "num"
            ]
          }
        }
      },
      {
        "name": "TOTEN",
        "type": "num",
        "value": "0.4",
        "ui": {
          "icon": "font-awesome/fa-spinner",
          "label": {
            "en-US": "toten"
          },
          "type": "input",
          "opts": {
            "types": [
              "num"
            ]
          }
        }
      },
      {
        "name": "VOLUME",
        "type": "num",
        "value": "1.0",
        "ui": {
          "icon": "font-awesome/fa-volume-up",
          "label": {
            "en-US": "volume"
          },
          "type": "spinner",
          "opts": {
            "min": 0,
            "max": 5
          }
        }
      },
      {
        "name": "INTONATION",
        "type": "num",
        "value": "1.0",
        "ui": {
          "icon": "font-awesome/fa-music",
          "label": {
            "en-US": "intonation"
          },
          "type": "spinner",
          "opts": {
            "min": 0,
            "max": 2
          }
        }
      },
      {
        "name": "URI",
        "type": "str",
        "value": "https://api.coefont.cloud/v1/text2speech",
        "ui": {
          "icon": "font-awesome/fa-globe",
          "type": "input",
          "opts": {
            "types": [
              "str"
            ]
          }
        }
      },
      {
        "name": "APIドキュメント：https://docs.coefont.cloud/",
        "type": "str",
        "value": "",
        "ui": {
          "type": "none"
        }
      }
    ],
    "meta": {},
    "credentials": {
      "SECRET": ""
    },
    "color": "#3FADB5",
    "icon": "font-awesome/fa-commenting"
  },
  {
    "id": "c714d2a12696eeba",
    "type": "switch",
    "z": "adcdb07133d05fd7",
    "name": "",
    "property": "payload",
    "propertyType": "msg",
    "rules": [
      {
        "t": "istype",
        "v": "buffer",
        "vt": "buffer"
      }
    ],
    "checkall": "false",
    "repair": false,
    "outputs": 1,
    "x": 300,
    "y": 80,
    "wires": [
      []
    ]
  },
  {
    "id": "7cb8f439e55c1230",
    "type": "function",
    "z": "adcdb07133d05fd7",
    "name": "",
    "func": "\nconst axios = new global.get('axios');\nconst crypto = new global.get('crypto');\n\nconst data = JSON.stringify({\n  'coefont': env.get(\"COEFONT\"),\n  'text': msg.payload,\n  'speed': env.get(\"SPEED\"),\n  'pitch': env.get(\"PITCH\") * 10,\n  'kuten': env.get(\"KUTEN\"),\n  'toten': env.get(\"TOTEN\"),\n  'volume': env.get(\"VOLUME\"),\n  'intonation': env.get(\"INTONATION\")\n});\nconst date = String(Math.floor(Date.now() / 1000));\n\nconst signature = crypto.createHmac('sha256', env.get(\"SECRET\")).update(date+data).digest('hex')\n\naxios.post(env.get(\"URI\"), data, {\n    headers: {\n      'Content-Type': 'application/json',\n      'Authorization': env.get(\"KEY\"),\n      'X-Coefont-Date': date,\n      'X-Coefont-Content': signature\n    },\n    responseType: 'arraybuffer'\n  })\n  .then(response => {\n    msg.payload = new Buffer(response.data, \"binary\");\n  })\n  .catch(error => {\n    console.log(error)\n  })\n  .finally(() => {\n    node.send(msg);\n  })\n\nreturn null;",
    "outputs": 1,
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 160,
    "y": 80,
    "wires": [
      [
        "c714d2a12696eeba"
      ]
    ]
  },
  {
    "id": "a17ed66939995391",
    "type": "subflow:adcdb07133d05fd7",
    "z": "c33d685b.315298",
    "name": "",
    "env": [
      {
        "name": "KEY",
        "value": "Set your Access Key",
        "type": "str"
      },
      {
        "name": "SECRET",
        "type": "cred"
      }
    ],
    "x": 660,
    "y": 1820,
    "wires": [
      [
        "61734d220179578a"
      ]
    ]
  },
  {
    "id": "fd521f8d8cfaddf9",
    "type": "inject",
    "z": "c33d685b.315298",
    "name": "",
    "props": [
      {
        "p": "payload"
      },
      {
        "p": "topic",
        "vt": "str"
      }
    ],
    "repeat": "",
    "crontab": "",
    "once": false,
    "onceDelay": 0.1,
    "topic": "",
    "payload": "",
    "payloadType": "date",
    "x": 300,
    "y": 1820,
    "wires": [
      [
        "7197bc148b5571ec"
      ]
    ]
  },
  {
    "id": "7197bc148b5571ec",
    "type": "template",
    "z": "c33d685b.315298",
    "name": "",
    "field": "payload",
    "fieldType": "msg",
    "format": "handlebars",
    "syntax": "mustache",
    "template": "これはテストです。",
    "output": "str",
    "x": 480,
    "y": 1820,
    "wires": [
      [
        "a17ed66939995391"
      ]
    ]
  },
  {
    "id": "61734d220179578a",
    "type": "play audio",
    "z": "c33d685b.315298",
    "name": "",
    "voice": "0",
    "x": 850,
    "y": 1820,
    "wires": []
  }
]
```

</div></details>

以前使っていたフローで喋らせている動画を掲載しておきます。結構楽しいですね。

https://twitter.com/shikasey/status/1420673823855181826?s=21

# おわりに
TTSと言えば「聞き取りづらい」「声の種類が少ない」「それなりの品質にすると利用料金が高い」というイメージが強かったのですが、CoeFont CLOUDの利用料金は安く、音声もかなり自然に近いと思います。
CoeFont CLOUDでは個人で活動されている方や有名な方まで幅広い方々が声を提供しています。

みなさんも、CoeFont CLOUDで応援したい方を見つけて、たくさん利用して推し活しましょう！！