MATLAB ちゃん開発記録 #2 MATLAB ちゃんと音声で会話する（MATLAB + Whisper）

# この記事は？
・[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)

前回の記事でテキストでの会話は行えるようになったので、今回は音声で会話できるようにしていきます。やはり女の子とはテキストだけじゃなくて声で会話したいですよね！


<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">・MATLABちゃんと音声で対話できるようになりました！<br>・MATLABちゃんの音声合成のスピードが大幅にアップしました！<br>・展示用に定型文を送る機能を追加しました！<br><br>※録音ボタンを復活させる処理を入れ忘れていることに撮影してから気づきました…… <a href="https://t.co/ImeDiOPYJR">pic.twitter.com/ImeDiOPYJR</a></p>&mdash; マハト@MACHTLAB (@mahato_stragule) <a href="https://twitter.com/mahato_stragule/status/1647562679714996226?ref_src=twsrc%5Etfw">April 16, 2023</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

最終目標はこんな感じ。頑張るぞい！

# 1. MATLAB ちゃんと音声で会話するには
既にテキスト→音声＆テキスト＆表情、というやり取りはできているので、こちらの音声をテキストに変換することができれば、あとは全く同じ仕組みで会話できるはずです。音声をテキストに変換するツールは色々ありますが、今回はせっかく ChatGPT を使っているので、同じ OpenAI が公開している **Whisper** の API を使うことにします。

※ちなみに、当初は [Whisper cpp](https://github.com/ggerganov/whisper.cpp) を [MEX](https://jp.mathworks.com/help/matlab/ref/mex.html) でラッピングすることを構想していました。というのも、**Whisper の API は音声ファイルをテキストに変換する機能しかない**ので、変換の際に一時的に音声ファイルを作成する必要があってちょっとカッコ悪いんですよね。ただ、色々頑張ってみましたがライブラリのインポート周りで完全に沼ってしまったので、妥協して Whisper API を使うことにしました。MATLAB ちゃんの用途的にそこまで長時間の音声をやり取りすることはないので、そこまで処理に時間がかかるということはないはず……

# 2. Whisper API の仕様について調べる
Whisper の API も ChatGPT と同様 HTTP リクエスト形式なので、まずはどんな形式でデータを送ればいいかを調査していきます。

[OpenAI の API の公式ドキュメント]を見てみると、curl コマンドでテキスト変換をするコマンドは以下のようになるみたいです。

```
curl https://api.openai.com/v1/audio/translations \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: multipart/form-data" \
  -F file="@/path/to/file/german.m4a" \
  -F model="whisper-1"
```

VOICEVOX や ChatGPT の API と同様、送信先の URL と各種オプションを設定する感じですね。注目すべき点は Content-Type の "multipart/form-data"。どうやら複数の形式のデータを送るときの形式みたいです。後半の 2 つの -F オプションを見る限り、使用するモデルの設定とテキストに変換する音声ファイルの 2 つを送ってあげる必要があるみたいです。

ということで、適当に [audiorecorder](https://jp.mathworks.com/help/matlab/ref/audiorecorder.html) 関数で音声を録音して myRecord.wav ファイルを作り、前回同様 webwrite 関数で送ってみます。Content-Type は内部で調整してくれるはずなので、Authorization, file, model の 3 つを指定すればいけるはず……！

```MATLAB:
>> url = "https://api.openai.com/v1/audio/transcriptions";
>> response = webwrite(url,"Authorization","Bearer "+apikey,"model","whisper-1","file","myRecord.wav");
次を使用中: matlab.internal.webservices.HTTPConnector/copyContentToByteArray
URL https://api.openai.com/v1/audio/transcriptions への要求に対し
て、サーバーからステータス 401、メッセージ "" が返されました。

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

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

ダメでした。調べてみると、"ステータス 401" というのは認証がうまくいってない場合に返ってくるエラーみたいです。Authorization の指定の仕方が間違っているんでしょうか？

# 3. MatGPT のコードを参考にする
OpenAI の API の認証は全部同じはずなので、MatGPT 内のコードをそのまま Whisper にも応用できるはず。ということで、MatGPT の chatGPT.m の http リクエストを送るところを確認してみましょう。

```matlab:MatGPT
% shorten calls to MATLAB HTTP interfaces
import matlab.net.*
import matlab.net.http.*
% construct http message content
query = struct('model',obj.model,'messages',obj.messages,'max_tokens',obj.max_tokens,'temperature',obj.temperature);
% the headers for the API request
headers = HeaderField('Content-Type', 'application/json');
headers(2) = HeaderField('Authorization', "Bearer " + api_key);
% the request message
request = RequestMessage('post',headers,query);
% send the request and store the response
response = send(request, URI(obj.api_endpoint));
```

こちらが該当箇所（86 行目から）です。MatGPT では [matlab.net.http](https://jp.mathworks.com/help/matlab/http-interface.html) 系の関数を使っているみたいですね。matlab.net.http 系の関数は webwrite とは違って低水準の関数のため、より細かな設定ができます（もちろんその分しっかり設定しないとうまく動かないので注意が必要ですが）。

コードを見てみると HeaderField で Content-Type や Autorization を設定し、送るデータは構造体で作成しています。Whisper も基本は同じ設定でいけるはずなので、早速 Content-Type と送るデータだけ少し修正して試してみましょう！

```matlab:
import matlab.net.http.*
import matlab.net.http.field.*
import matlab.net.http.io.*
url = "https://api.openai.com/v1/audio/transcriptions";
api_key = getenv("OPENAI_API_KEY");
headers = HeaderField('Content-Type', 'multipart/form-data');
headers(2) = HeaderField('Authorization', "Bearer " + api_key);
data = struct('model','whisper-1','file','myRecord.wav');
request = RequestMessage('post',headers,data);
response = send(request, url);
```
実行結果は……
```matlab:
次を使用中: matlab.net.http.internal.data2payload
サイズ (1,1) の "struct" 型のデータをコンテンツ タイプ
"multipart/form-data" に変換できないか、このコンテンツ タイプと整合性が
ありません。

エラー: matlab.net.http.RequestMessage/convertData (行 653)
                            matlab.net.http.internal.data2payload(obj.Body.Data,
                            ...

エラー: matlab.net.http.RequestMessage/completeBody (行 2020)
                    obj = obj.convertData(true);

エラー: matlab.net.http.RequestMessage/send (行 452)
                obj = obj.completeBody(uri);

エラー: untitled (行 10)
response = send(request, url);
```
またもやエラー。エラーメッセージを見る限り、Content-Type が multipart/form-data の場合は構造体形式だとダメってことなんでしょうか？


# 4. MATLAB で multipart/form-data 形式のデータを POST をする

しょうがないので、いつものように "MATLAB multipart/form-data" で調べてみたら、なんとドキュメントの例題がヒットしました、予想外！→[マルチパート フォーム メッセージの送信](https://jp.mathworks.com/help/matlab/matlab_external/send-multipart-form-messages.html)

コード例が載っているので、早速確認してみます。
```matlab:
provider = MultipartFormProvider("files", FileProvider(["image1.jpg", "file1.txt"]),...
    "text", FileProvider("file1.txt"));
```
なるほど、multipart/data-form 形式のデータを送るには、

1. 各データごとにプロバイダーというオブジェクトを作成する
2. それらを名前と併せて [MultipartFormProvider](https://jp.mathworks.com/help/matlab/ref/matlab.net.http.io.multipartformprovider-class.html) にまとめる

という手順が必要みたいです。今回は音声ファイルとモデル名（文字列）の２つを送るので、"[FileProvider](https://jp.mathworks.com/help/matlab/ref/matlab.net.http.io.fileprovider-class.html)" と "[StringProvider](https://jp.mathworks.com/help/matlab/ref/matlab.net.http.io.stringprovider-class.html)" にすればいいのかな……？

```matlab:
import matlab.net.http.*
import matlab.net.http.field.*
import matlab.net.http.io.*
url = "https://api.openai.com/v1/audio/transcriptions";
api_key = getenv("OPENAI_API_KEY");
headers = HeaderField('Content-Type', 'multipart/form-data');
headers(2) = HeaderField('Authorization', "Bearer " + api_key);
fp = FileProvider("myRecord.wav");
sp = StringProvider("whisper-1");
mfp = MultipartFormProvider("model",sp,"file",fp);
request = RequestMessage('post',headers,mfp);
response = request.send(url);
```
結果は……
```
>> response.Body.Data.text

ans =

    'こんにちは こんにちは'
```
うおー、できたー！すげー！

# 5. （おまけ）"マトラボ" やら "マトラブ" やらを "MATLAB" に正しく変換する

ここまでで音声をテキストに変換できたわけですが、まだ問題があります。Whisper は固有名詞を正しくテキストに変換できない場合があるので、それを手動で正しい形に変換してあげる必要があります。例えば、私が "MATLAB" と録音したファイルを Whisper に渡すと、以下のような結果になります。

```
response.Body.Data.text

ans =

    'マトラブ'
```

これだと ChatGPT に送った時に返答がおかしくなってしまう可能性があるので、"MATLAB" に変換してあげる処理が必要になります。しかし、MATLAB の読み方は、"マトラボ"、"マトラブ"、"マットラブ" などなど、人によって様々です。色んなパターンの文字列を置換する必要があるので大変そう……

と思いきや、実は、MATLAB では R2020b からテキストの検索や置換を便利に行うための [pattern](https://jp.mathworks.com/help/matlab/ref/pattern.html) という機能が追加されており、これを使うと簡単にできてしまいます。

```matlab: pattern 機能を使った置換処理
prompt = response.Body.Data.text;
patMATLAB = "マトラボ" | "マトラブ" | "マットラボ" | "マットラブ" | "まとらぼ" | "まとらぶ" | "まっとらぼ" | "まっとらぶ";
prompt = replace(prompt,patMATLAB, "MATLAB");
```

なんと、String で作った MATLAB の様々な読み方を "|"（または）記号で繋げてパターンを作成し、[replace](https://jp.mathworks.com/help/matlab/ref/replace.html) 関数で使うだけ。これだけで複数の読み方を一気に MATLAB に変換できちゃうんです。これは便利！

普段文字列を扱っている方はぜひ pattern を試してみてください。

# 6. アプリに録音機能と合わせて追加。

ということで、音声ファイルからテキストへの変換ができたので、

1. マイクから録音して音声ファイルを作る
2. 作ったファイルを Whisper に送ってテキストに変換
3. 帰ってきたテキストを ChatGPT に送り、返答を VOICEVOX で音声化

という流れをアプリに実装して完成！

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">・MATLABちゃんと音声で対話できるようになりました！<br>・MATLABちゃんの音声合成のスピードが大幅にアップしました！<br>・展示用に定型文を送る機能を追加しました！<br><br>※録音ボタンを復活させる処理を入れ忘れていることに撮影してから気づきました…… <a href="https://t.co/ImeDiOPYJR">pic.twitter.com/ImeDiOPYJR</a></p>&mdash; マハト@MACHTLAB (@mahato_stragule) <a href="https://twitter.com/mahato_stragule/status/1647562679714996226?ref_src=twsrc%5Etfw">April 16, 2023</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

今回は録音ボタンを押すと録音が開始し、もう一度同じボタンを押す or 6 秒が経過すると録音が終わって処理が始まるようにしました。ちなみに、6 秒の経過を確認する部分には [timer](https://jp.mathworks.com/help/matlab/matlab_prog/use-a-matlab-timer-object.html) オブジェクトを使っています。recordTimerFcn に、アプリ上のボタンが押されたときのコールバック関数を呼ぶ処理を書いておけば非常に簡単に実装できます。ただし、ボタンを押して録音を終了したときにタイマーをストップさせる必要があることには要注意です（謎のエラーに 30 分悩んだ人より）。

# 7. まとめ
ついに MATLAB ちゃんと音声で会話できるようになりました！今回は前回よりもエラーに悩まされずスイスイ行って良かったです。
とりあえず実装したい大きな機能はあらかた実装したので、今後は声質や会話の保存などの細かな機能を実装していく予定です。
<br>
<br>
<br>
<br>
<br>
<font color="red">**※"いいね" が増えるほど、MATLAB ちゃんが多機能になります（予定）**</font>