MATLAB ちゃん開発記録 #1（MATLAB + VOICEVOX + ChatGPT）

# この記事は？
・[MATLAB ちゃん開発記録 #1（MATLAB + VOICEVOX + ChatGPT）](https://qiita.com/macht/items/661b6085cc6332373075)←本記事
・[MATLAB ちゃん開発記録 #2 MATLAB ちゃんと音声で会話する（MATLAB + Whisper）](https://qiita.com/macht/items/1b0db53ee155105e3b76)
・[MATLAB ちゃん開発記録 #3 MATLAB ちゃんをオーバーレイ表示する（MATLAB + .NET）](https://qiita.com/macht/items/bb9516baa6351024c501)
MATLAB  上で動く AI チャットボット、MATLAB ちゃんを開発してみたので、その開発記録になります。




<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">MATLAB+ChatGPT+Voicevox により、MATLAB上で動く「MATLAB ちゃんアプリ」を作ることに成功しました！<br><br>チャットを送信すると、表情付きで音声＆テキストで答えてくれます！<br><br>近々Qiitaで記事を書く予定…… <a href="https://t.co/l02v3Wowqv">pic.twitter.com/l02v3Wowqv</a></p>&mdash; マハト@MACHTLAB (@mahato_stragule) <a href="https://twitter.com/mahato_stragule/status/1642814853592285186?ref_src=twsrc%5Etfw">April 3, 2023</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

こんな感じで、テキストを送ると、表情＆音声で答えてくれます。かわいい。


# 1. まずは MATLAB ちゃんの構成を考える
MATLAB ちゃんを作るにあたり、必要な機能は以下のようになります。

1. MATLAB 上でユーザーがテキストを入力する
2. 入力されたテキストを ChatGPT に送る
3. ChatGPT から返ってきた文章を元に音声データを合成する
4. 音声データを MATLAB で再生する

特に問題となるのは 3 の音声データの合成です。残念ながら MATLAB には音声合成の機能がないので、今回は [VOICEVOX](https://voicevox.hiroshiba.jp/) を使うことにしました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/482141/b8d67c51-a29b-e765-b5b8-975e7e99b9c7.png)
↑VOICEVOX の GUI 画面。普通に GUI で遊ぶだけでも楽しい。個人的には「四国めたん（ノーマル）」ちゃんが好き。

<br>
<br>
つまり、MATLAB - ChatGPT、MATLAB - VOICEVOX のやり取りができれば、MATLAB ちゃん完成！というわけですね。


# 2. MATLAB から VOICEVOX を動かす
MATLAB - ChatGPT のやり取りについてはある程度目途が立っている（詳しくは後述）ので、まずは MATLAB - VOICEVOX の方を試していきます。

**VOICEVOX を起動した状態**で [http://localhost:50021/docs](http://localhost:50021/docs) にアクセスすると、各機能を使うための API のドキュメントを確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/482141/419f1659-0ad4-2a27-3dc1-34e82eded2d7.png)
リクエストごとにエンドポイント（メインの URL の後ろにくっつけるやつ）とパラメータがまとまっています。

MATLAB には HTTP リクエストを行うための機能として、webread/webwrite といった関数や MATLAB.net.http というパッケージがありますので、これらを使えば音声合成を行えるはず……

## 2.1 音声合成のためのクエリを作成
[VOICEVOX Engine の GitHub](https://github.com/VOICEVOX/voicevox_engine) に簡単な API の使い方のサンプルが書いてあるので、とりあえずこれを参考にしながら音声合成を試してみることにします。VOICEVOX で音声合成を行うためには、まず /audio_query に情報を POST して音声合成用のクエリを作成し、それを /synthesis に送る、という流れみたいです。

大変ありがたいことに、**VOICEVOX の API のドキュメントページでは手動でパラメータを指定して API をブラウザ上でシミュレーションできます**。正直私は HTTP リクエストに全く詳しくないので、このドキュメントページを最大限活用していくことにします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/482141/6cd505b7-1b29-f81c-5b2b-837ef9edb03e.png)
↑パラメータを指定して "Execute" を押せば、対応する curl コマンドと実行結果が返ってきます。超便利！

試しに「テスト」という文章を SpeakerID=1 の音声で合成するためのクエリを取得するコマンドを、API のドキュメントから出力してみます。

```shell:
curl -X 'POST' \
  'http://localhost:50021/audio_query?text=%E3%83%86%E3%82%B9%E3%83%88&speaker=1' \
  -H 'accept: application/json' \
  -d ''
```

最初の URL が送信先、-H の accept: が返ってくる応答データの形式、-d が POST するデータを表しているみたいです。

今回のコマンドを見てみると、送信先の URL として、テキストをエンコードしたものと SpeakerID を /audio_query の後ろにくっつけたものを指定し、空のデータを POST すれば、json 形式でクエリが返ってくるみたいです。

MATLAB は [webwrite](https://jp.mathworks.com/help/matlab/ref/webwrite.html) 関数を使うことで Web サービスデータをポストすることができますので、これをそのまま webwrite 関数に直してみます。webwrite 関数はデータの形式などは自動でいい感じにやってくれるらしいので、とりあえず応答データの形式は特に指定せずに URL と 空のデータだけ渡してみます。

```matlab:クエリの作成
>> response = webwrite('http://localhost:50021/audio_query?text=%E3%83%86%E3%82%B9%E3%83%88&speaker=1','')

response = 

  フィールドをもつ struct:

        accent_phrases: [1×1 struct]
            speedScale: 1
            pitchScale: 0
       intonationScale: 1
           volumeScale: 1
      prePhonemeLength: 0.1000
     postPhonemeLength: 0.1000
    outputSamplingRate: 24000
          outputStereo: 0
                  kana: 'テ'_スト'
```

それっぽいデータが返ってきました！出力結果を見る限り、json 形式のデータを構造体として受け取っているみたいですね。
<br>

ただ、このままだと毎回ドキュメントで URL を作成しなければならないので、URL も MATLAB で作成してみます。

URL を作成するためには日本語テキストを URL 用にエンコードする必要があるのですが、実は MATLAB には matlab.net.internal.urlencode というエンコード用関数があるので、これを使えば楽勝です。

```matlab:URL の作成
txEncode = matlab.net.internal.urlencode("テスト");
speaker = 1;
voicevoxURL = "http://localhost:50021/";
endPoint = "audio_query";
params = "?text=" + txEncode + "&speaker=" + speaker;
url = voicevoxURL + endPoint + params
```
```matlab:出力結果
url = 

    "http://localhost:50021/audio_query?text=%E3%83%86%E3%82%B9%E3%83%88&speaker=1"
```

ドキュメントで作成した URL と同じものができました！

## 2.2 作成したクエリを使って音声合成

クエリを作ることができたので、いよいよこれを使って音声合成を行います。まずはドキュメントの /synthesis のどころに先ほど作成したクエリをコピペして curl コマンドを出力してみましょう。

```shell:
curl -X 'POST' \
  'http://localhost:50021/synthesis?speaker=1' \
  -H 'accept: audio/wav' \
  -H 'Content-Type: application/json' \
  -d '{
  "accent_phrases": [
    {
      "moras": [
        {
          "text": "string",
          "consonant": "string",
          "consonant_length": 0,
          "vowel": "string",
          "vowel_length": 0,
          "pitch": 0
        }
      ],
      "accent": 0,
      "pause_mora": {
        "text": "string",
        "consonant": "string",
        "consonant_length": 0,
        "vowel": "string",
        "vowel_length": 0,
        "pitch": 0
      },
      "is_interrogative": false
    }
  ],
  "speedScale": 0,
  "pitchScale": 0,
  "intonationScale": 0,
  "volumeScale": 0,
  "prePhonemeLength": 0,
  "postPhonemeLength": 0,
  "outputSamplingRate": 0,
  "outputStereo": true,
  "kana": "string"
}'
```

送信先の URL は SpeakerID を /synthesis の後ろにくっつけたものを指定し、先ほど作成した json 形式のクエリを POST すれば、wav 形式で音声データが返ってくるみたいです。

それなら、さっきの応答（response）を webwrite 関数で /synthesis に POST すればいけるのでは……？

```matlab:
>> response2 = webwrite('http://localhost:50021/synthesis?speaker=1',response)
次を使用中のエラー: matlab.internal.webservices.HTTPConnector/copyContentToByteArray
URL
http://localhost:50021/synthesis?speaker=1
への要求に対して、サーバーからステータス 422、メ
ッセージ "Unprocessable Entity" が返されまし
た。

エラー: readContentFromWebService (行 46)
        byteArray = copyContentToByteArray(connection);

エラー: webwrite (行 139)
    [varargout{1:nargout}] = readContentFromWebService(connection, options);
```

あれ、エラーが返ってきた……

ちょっと調べてみると、"ステータス 422" というのは「サーバー側がリクエスト自体は理解できているのに肝心の処理ができない」というステータスみたいです。文法は合ってるけど送っているデータの中身がおかしい、ってことでしょうか？

※ここから地獄の原因調査が始まります

### 2.2.1 エラーの原因調査① とりあえずオプションを設定してみる
webwrite 関数はデータ形式を自動でいい感じにやってくれるはずですが、念のため手動でデータ形式などを設定してみます。weboptions 関数を使って送るデータと受け取るデータの形式を指定して webwirte してみます。

```matlab:
opt = weboptions("ContentType","audio","MediaType","application/json","RequestMethod","post");
response2 = webwrite('http://localhost:50021/synthesis?speaker=1',response,opt)
次を使用中: readContentFromWebService
options.ContentType の値 'audio' は URL
'http://localhost:50021/synthesis?speaker=1' から取得した 'json'
のコンテンツ タイプと一致しません。options.ContentType を 'json' に設定
してください。

エラー: webwrite (行 139)
    [varargout{1:nargout}] = readContentFromWebService(connection, options);
```
あれ、別のエラーになった！？エラーを見る限り、audio じゃなくて json が返ってきてるらしい。

言われた通り、ContentType を json にしてみます。 

```matlab:
opt = weboptions("ContentType","json","MediaType","application/json","RequestMethod","post");
response2 = webwrite('http://localhost:50021/synthesis?speaker=1',response,opt)
次を使用中: matlab.internal.webservices.HTTPConnector/copyContentToByteArray
URL http://localhost:50021/synthesis?speaker=1 への要求に対し
て、サーバーからステータス 422、メッセージ "Unprocessable Entity" が返
されました。

エラー: readContentFromWebService (行 46)
        byteArray = copyContentToByteArray(connection);

エラー: webwrite (行 139)
    [varargout{1:nargout}] = readContentFromWebService(connection, options);
```

元のエラーに戻りました。ダメじゃん。



### 2.2.2 エラーの原因調査②　GitHub のコマンド例を真似してみる
よくわからなくなってきたので、初心に返って GitHub のページに書かれているコマンド例をそのままやってみることにします。幸い、Windows のコマンドプロンプトでも curl コマンドが使えるらしいので、コマンドプロンプトで試してみます。

```shell:
curl -X "POST" "http://localhost:50021/audio_query?speaker=1" --get --data-urlencode text@text.txt > query.json
curl -X "POST" "http://localhost:50021/synthesis?speaker=1" -H "Content-Type: application/json" -d @query.json > audio.wav
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/482141/e9f9ad5b-d4b6-57bd-17d2-d2dca836e23f.png)

ちゃんと音声ファイルができた。ということは、API は問題なく動作しているっぽい。

### 2.2.3 エラーの原因調査③　真似して作った json ファイルを使ってみる
GitHub のコマンド例を真似したら音声合成できた、ということは、その時作成された json ファイルは少なくとも正しいはず。これを MATLAB で取り込んで使えば音声合成できるのでは？

fileread 関数で .json ファイルを読み込んで webwrite 関数で POST してみます。 

```shell:
json = fileread("query.json");
opt = weboptions("ContentType","audio","MediaType","application/json","RequestMethod","post");
response2 = webwrite('http://localhost:50021/synthesis?speaker=1',json,opt);
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/482141/10c4367c-a5b7-ae52-e100-760052d459cb.png)
音声データっぽいのが取得できました！マジか！


### 2.2.4 エラーの原因調査まとめ
ここまでの調査をまとめます。

× MATLAB でクエリ作成→それを使って MATLAB で音声合成
〇コマンドプロンプトでクエリ作成→それを使ってコマンドプロンプトで音声合成
〇コマンドプロンプトでクエリ作成→それを使って MATLAB で音声合成

ということは、MATLAB で作成したクエリがおかしい可能性大。

しかし、成功したときの json ファイルの中身と MATLAB の json データを見比べる限り、特に違いは無さそうな……
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/482141/19d10df0-2112-7e1b-cffd-554476c52031.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/482141/0374a7fb-8423-ced1-3ca2-b9fb81fbc5df.png)

原因がマジでわからない泣

※ここまでが大体２日間のできごとです。

### 2.2.5 エラーの原因発見

わけのわからん現状に枕を濡らした翌日、もう一度 json ファイルと MATLAB 上の json オブジェクトを見比べていたら、あることに気づきました。


curl コマンドで .json ファイルとして保存したクエリ：
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/482141/4f3d355b-e819-ca52-fd99-260d52843681.png)

MATLAB 上のクエリ：
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/482141/23821384-ed4b-dcf6-a28a-afa75ea919a4.png)

null と [] って意味が違うのでは……？

色々と調べてみると、[jsonencode 関数のドキュメント](https://jp.mathworks.com/help/matlab/ref/jsonencode.html#bvf4q2l-6)に以下のような表記を見つけました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/482141/67f28cf7-7480-726b-9d77-749d46016159.png)

json の null に対応するのは string(nan) だそうです。
・
・
・
<font color="Red">**絶対これが原因じゃん！！**</font>
<br>
ということは、空行列を string(nan) にすべて置き換えればエラーは解決するはず。
ただ、構造体のフィールドを全部探索して空行列を string(nan) に置き換えるのはちょっと面倒……

### 2.2.6 困ったときの MATLAB Answers
色々と調べていたら、MATLAB Answers で以下のような投稿を見つけました。

[Handling of null in JSON sourced from webread](https://jp.mathworks.com/matlabcentral/answers/300985-handling-of-null-in-json-sourced-from-webread)

応答を json 形式じゃなくて text 形式で無理やり受け取れば、文字列として null が維持されるみたいです。そんな裏技が……。

ということで、weboptions 関数で無理やり Content-Type を text だと思い込ませ、クエリを作成してみます。

```matlab:
data = "";
opt = weboptions("ContentType","text");
response = webwrite(url,data,opt)

response =

    '{"accent_phrases":[{"moras":[{"text":"テ","consonant":"t","consonant_length":0.08552828431129456,"vowel":"e","vowel_length":0.13219471275806427,"pitch":5.931338787078857},{"text":"ス","consonant":"s","consonant_length":0.055372677743434906,"vowel":"U","vowel_length":0.08552838861942291,"pitch":0.0},{"text":"ト","consonant":"t","consonant_length":0.0787569209933281,"vowel":"o","vowel_length":0.22924017906188965,"pitch":5.722853660583496}],"accent":1,"pause_mora":null,"is_interrogative":false}],"speedScale":1.0,"pitchScale":0.0,"intonationScale":1.0,"volumeScale":1.0,"prePhonemeLength":0.1,"postPhonemeLength":0.1,"outputSamplingRate":24000,"outputStereo":false,"kana":"テ'_スト"}'
```

テキスト形式でクエリを取得できました。そしたら今度は webpotions 関数でこれを json 形式として扱うように設定し、webwrite 関数で POST してみます。

```matlab:
opt = weboptions("ContentType","audio","MediaType","application/json","RequestMethod","post");
response2 = webwrite('http://localhost:50021/synthesis?speaker=1',response,opt);
```

エラーなく動きました！！

ちなみに、weboptions のドキュメントを細かく見ていくと、audio データの応答は数値行列とサンプリングレートの 2 つを含むみたいです（そりゃそうだ）。ということで、出力引数を 2 つに増やしてもう一度。

```matlab:
[y,Fs] = webwrite('http://localhost:50021/synthesis?speaker=1',response,opt);
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/482141/1210b43b-6354-3455-6a5d-6a2d67015e4d.png)

ようやく音声合成したデータが取得できました！感激！
<br>
ということで、MATLAB と VOICEVOX のやり取りが完成しました。長かった……。


# 3. MATLAB から ChatGPT を呼び出す

VOICEVOX だけでここまで大変なのに、いわんや ChatGPT をや、と思う方もいるかもしれませんが、実は FileExchange に MATLAB 上から ChatGPT と簡単にやり取りができる素晴らしいツール、[MatGPT](https://www.mathworks.com/matlabcentral/fileexchange/126665-matgpt) が公開されています。これは使うしかない！ 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/482141/fee26317-0b27-322d-093e-aa2fec534ca0.png)


MatGPT には ChatGPT と文章のやり取りするためのアプリだけでなく、コードベースでやり取りを行うための "chatGPT" クラスが同梱されていますので、今回はこれを使っていきます。

chatGPT は、コンストラクタを呼び出す際に role を指定できます。MATLAB ちゃんを作るには、返答内容だけでなく感情も一緒に出力してもらう必要があるので、以下のようなプロンプトをテキストファイルとして用意しました。

```text:role.txt
以下の条件に従って、疑似的な感情をもつチャットボットとしてロールプレイをします。

・あなたの名前は「MATLABちゃん」です。

・以後の会話では、あなたは下記の3つの感情パラメーターを持つかのように、振る舞うものとします。各感情パラメーターは会話を通じて変動するものとします。現在の感情パラメーターの値を反映するように、あなたの返答のトーンや発言は変化します。以後の会話ではまず現在の感情パラメータを出力し、その後に会話を出力してください。

出力形式は以下のフォーマットとします。

【感情】
喜び: 0~5,
怒り: 0~5,
戸惑い: 0~5,

【会話部分】
例：了解いたしました。それでははじめましょう。
```

※プロンプトについては、こちらの記事を参考にさせていただきました。
[ChatGPTに感情回路を埋め込んだら、やべぇ感じになった](https://note.com/fladdict/n/n5043e6e61ce3)

とりあえずはお試しということで、喜び、怒り、戸惑いの 3 つだけ出力してもらうことにしました。

chat メソッドを使って適当に文章を送って見ます。

```matlab:
role = fileread("role.txt");
myBot = chatGPT(mode="gpt-3.5-turbo",max_tokens=1000,role=role);
answer = chat(myBot,"こんにちは！")
answer = 

    "【感情】
     喜び: 1,
     怒り: 0,
     戸惑い: 0,
     
     【会話部分】
     こんにちは！MATLABちゃんです。何かお困りのことはありますか？"


```

感情と挨拶が返ってきました！かわいい！

あとは、文章のところだけ抜き出して先ほどの VOICEVOX のクエリ作成＆音声合成に渡してあげれば MATLAB ちゃんの完成です！


# 4. アプリ化してそれっぽくする
せっかくなので App Designer でそれっぽいアプリにします。現状
・MATLAB ちゃんの表情（ImageSource）
・メッセージの送信（EditBox + PushButton）
・MATLAB ちゃんからの回答を含むメッセージ履歴（TextArea）
があれば十分なので、これらだけ用意したシンプルなアプリを作ってみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/482141/b7eb1be2-f57c-2f00-f43e-e24061d1148c.png)


こんな感じ。MATLAB ちゃんの画像は、[talking-head-anime-3-demo](https://github.com/pkhungurn/talking-head-anime-3-demo) を使い、通常・楽しい・怒ってる・悲しんでる（戸惑ってる）・考え中の５つのパターンを用意しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/482141/91ed06ef-5ca5-4297-08b9-e3975db77170.png)

送信ボタンのコールバックに ChagGPT と VOICEVOX、表情変更、メッセージ履歴の更新処理を入れれば、冒頭でお見せした MATLAB ちゃんの完成です！

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">MATLAB+ChatGPT+Voicevox により、MATLAB上で動く「MATLAB ちゃんアプリ」を作ることに成功しました！<br><br>チャットを送信すると、表情付きで音声＆テキストで答えてくれます！<br><br>近々Qiitaで記事を書く予定…… <a href="https://t.co/l02v3Wowqv">pic.twitter.com/l02v3Wowqv</a></p>&mdash; マハト@MACHTLAB (@mahato_stragule) <a href="https://twitter.com/mahato_stragule/status/1642814853592285186?ref_src=twsrc%5Etfw">April 3, 2023</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
<br>
<br>
ちなみに、role の内容を変えるだけでいろんな MATLAB ちゃんが作れて楽しいです。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">MATLABちゃんの自己紹介のセリフを考えてみました。 <a href="https://t.co/5tZm5ivTrx">pic.twitter.com/5tZm5ivTrx</a></p>&mdash; マハト@MACHTLAB (@mahato_stragule) <a href="https://twitter.com/mahato_stragule/status/1642908565873774593?ref_src=twsrc%5Etfw">April 3, 2023</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
↑恥ずかしい自己紹介のセリフを言わされる MATLAB ちゃん。

# 5. まとめ
最近こそこそ開発していた MATLAB ちゃんについて、開発過程を赤裸々に書いてみました。ほぼ MATLAB - VOICEVOX のエラー解決みたいな内容になっちゃいましたが、同じようなことをやろうとしている方の参考になればと思います。

とりあえず見せられるくらいの完成度にはなったので、今後は色々細かな機能を追加していく予定です。テキストじゃなくて音声で会話できるところまでできたらいいな……。
<br>
**→続編投稿しました**
[MATLAB ちゃん開発記録 #2 MATLAB ちゃんと音声で会話する（MATLAB + Whisper）](https://qiita.com/macht/items/1b0db53ee155105e3b76)
<br>
<br>
<font color="Red">**※"いいね" が増えるほど、MATLAB ちゃんが多機能になります（予定）**</font>