ChatGPTを用いた発声練習補助ツールを作成してみた。

[私](https://moov.ooo/article/645a1b0692174f167ab055b4)は、発話出来ないので、[このようなツール](https://www.tmd.ac.jp/dept-drh/information/VoiceRetriever.html)で発声練習しているのですが、ChatGPTで発声練習用のテキストを生成して、発声練習した文と音声をデータとして、保存するツールを作成してみました。

ツール内容としては、以下です。
ユーザーがLINEに送付したテキストから、ChatGPTが発声練習用のテキストを生成する
↓
ユーザーがLINEでChatGPTが生成した発声練習用のテキストを読み上げて録音したら、録音した音声とChatGPTが生成した発声練習用のテキストをインスタントの指定したディレクトリに保存する

今回もAWSのインスタンスイメージがubuntuのインスタンスを使用しています。

手順は、ツールを起動中にするだけです。

# ツールを起動中にする
まず、インスタンスにSSH接続します。

インスタンスのコンソールが開いたら以下をインストールします。

aptのアップデート
```
sudo apt update
```

ffmpegのインストール
```
sudo apt-get install ffmpeg
```

pipのインストール
```
sudo apt install python3-pip
```

LINEのインストール
```
sudo pip install line-bot-sdk
```

ngrokのインストール
```
sudo pip install pyngrok
```

flaskのインストール
```
sudo pip install flask
```

openaiのインストール
```
sudo pip install openai
```

pydubのインストール
```
sudo pip install pydub
```

データを保存するディレクトリを作成します。
```
mkdir ディレクトリ名
```

作成したディレクトリに移動します。
```
cd ディレクトリ名
```

テキスト保存用のディレクトリを作成します。
```
mkdir ディレクトリ名
```

音声保存用のディレクトリを作成します。
```
mkdir ディレクトリ名
```

一度、ホームディレクトリに戻ります。
```
cd
```

ホームディレクトリに戻ったら、pythonのファイルを保存するディレクトリを作成します。
```
mkdir ディレクトリ名
```

pythonのファイルを保存するディレクトリに移動します。
```
cd ディレクトリ名
```

作成したディレクトリでファイルを作成します。
```
nano ファイル名.py
```

作成したファイルの中に下記のbotのコードを入れて、ctrl oでファイルを保存して、ctrl xでファイルを閉じます。
※先程作成したテキストと音声を保存するディレクトリをそれぞれコード内で指定してください。
※LINE、ngrok、openaiのキーやトークンをコードに入れてください。これらの取得については、[こちら](https://qiita.com/katsuki_ono/items/df8830c8b07e3a59f354)を参考にしてください。

```python
# LINEのチャネルシークレット
LINE_CHANNEL_SECRET = ''
# LINEのチャネルアクセストークン
LINE_ACCESS_TOKEN = ''
# ngrokのAuthtoken
NGROK_AUTHTOKEN = ''
# openaiのキー
OPENAI_API_KEY = ""

import os
import datetime
import tempfile
import wave
import io
from io import BytesIO
import requests
from flask import Flask, request, abort
from linebot import LineBotApi, WebhookHandler
from linebot.exceptions import InvalidSignatureError
from linebot.models import MessageEvent, AudioMessage, TextSendMessage, TextMessage, ImageSendMessage
import pydub
from pydub import AudioSegment
import openai
from pyngrok import ngrok
from pyngrok.conf import PyngrokConfig


# URLの生成
ngrok.set_auth_token(NGROK_AUTHTOKEN)
os.system('kill -9 $(pgrep ngrok)')
https_tunnel = ngrok.connect(addr='127.0.0.1:5000',bind_tls=True)
print(https_tunnel)


app = Flask(__name__)


# テキストファイルと音声ファイルの保存先のディレクトリ
text_dir_path = "/path/to/your/directory"
audio_dir_path = "/path/to/your/directory"


openai.api_key = OPENAI_API_KEY
line_bot_api = LineBotApi(LINE_ACCESS_TOKEN)
handler = WebhookHandler(LINE_CHANNEL_SECRET)

@app.route("/test")
def test():
    return "TEST OK"

@app.route("/", methods=['POST'])
def callback():
    # get X-Line-Signature header value
    signature = request.headers['X-Line-Signature']

    # get request body as text
    body = request.get_data(as_text=True)
    app.logger.info("Request body: " + body)

    # handle webhook body
    try:
        handler.handle(body, signature)
    except InvalidSignatureError:
        print("Invalid signature. Please check your channel access token/channel secret.")
        abort(400)

    return 'OK'


def delete_latest_files(user_id):
    user_text_files = [os.path.join(text_dir_path, file) for file in os.listdir(text_dir_path) if file.startswith(user_id) and file.endswith('.txt')]
    user_audio_files = [os.path.join(audio_dir_path, file) for file in os.listdir(audio_dir_path) if file.startswith(user_id) and file.endswith('.wav')]

    if not user_text_files or not user_audio_files:
        return False

    latest_text_file = max(user_text_files, key=os.path.getctime)
    latest_audio_file = max(user_audio_files, key=os.path.getctime)

    if os.path.splitext(os.path.basename(latest_text_file))[0] == os.path.splitext(os.path.basename(latest_audio_file))[0]:
        os.remove(latest_text_file)
        os.remove(latest_audio_file)
        return True
    else:
        return False


user_deletion_statuses = {}  # ユーザーIDをキーとして、直前の削除要求状態を格納

@handler.add(MessageEvent, message=TextMessage)
def handle_text_message(event):
    # 受信したテキストをChatGPTで処理する
    text = event.message.text
    user_id = event.source.user_id

    if text.lower() == "削除":
        if user_id not in user_deletion_statuses or not user_deletion_statuses[user_id]:
            if delete_latest_files(user_id):
                reply_text = "最新のテキストファイルと音声ファイルを削除しました。"
                user_deletion_statuses[user_id] = True
            else:
                reply_text = "対応するデータが存在せず、データを削除できません。"
        else:
            reply_text = "既にデータを削除しました。テキストを入力してください。"
    else:
        temp_file_name = f"{user_id}.temp.txt"
        temp_file_path = os.path.join(text_dir_path, temp_file_name)

        if os.path.exists(temp_file_path):
            with open(temp_file_path, "r") as f:
                reply_text = f.read()
        else:
            response = openai.ChatCompletion.create(
                model="gpt-3.5-turbo",
                messages=[
                    {"role": "system", "content": "以下をテーマに五十音が満遍なく含まれる日本語の発声練習用の200文字以内の例文を作成する。例文のみ出力する。例文の説明は不用。"},
                    {"role": "user", "content": text},
                ]
            )
            reply_text = response["choices"][0]["message"]["content"]

            with open(temp_file_path, "w") as f:
                f.write(reply_text)

        user_deletion_statuses[user_id] = False

    line_bot_api.reply_message(event.reply_token, TextMessage(text=reply_text))


@handler.add(MessageEvent, message=AudioMessage)
def handle_audio_message(event):
    user_id = event.source.user_id
    text_file_name = f"{user_id}.temp.txt"
    text_file_path = os.path.join(text_dir_path, text_file_name)

    if os.path.exists(text_file_path):
        # Get audio data from LINE message
        audio_data = line_bot_api.get_message_content(event.message.id).content

        # Convert the audio data to a compatible format
        audio_segment = AudioSegment.from_file(io.BytesIO(audio_data), format="m4a")
        audio_segment = audio_segment.set_frame_rate(16000).set_channels(1)

        # Save the audio data as a WAV file
        audio_received_time = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
        audio_file_name = f"{user_id}.{event.message.id}.{audio_received_time}.wav"
        audio_file_path = os.path.join(audio_dir_path, audio_file_name)

        with wave.open(audio_file_path, "wb") as wf:
            wf.setnchannels(audio_segment.channels)
            wf.setsampwidth(audio_segment.sample_width)
            wf.setframerate(audio_segment.frame_rate)
            wf.writeframes(audio_segment.get_array_of_samples().tobytes())

        new_text_file_name = f"{user_id}.{event.message.id}.{audio_received_time}.txt"
        new_text_file_path = os.path.join(text_dir_path, new_text_file_name)

        os.rename(text_file_path, new_text_file_path)

        line_bot_api.reply_message(event.reply_token, TextMessage(text="音声データを保存しました。"))
    else:
        line_bot_api.reply_message(event.reply_token, TextMessage(text="テキストファイルがありません。"))

if __name__ == "__main__":
    app.run()
```

一度、上手くファイルが作成されているか下記のコマンドで確認します。
```
find ~
```

上手くファイルが作成されていたら、screenを作成します。
```
screen -S スクリーン名
```

ルート権限で、screen内でpythonで作成したファイルを実行します。
```
sudo python3 ファイル名.py
```

実行したら、生成されたURLを取得して、LINE Developers のダッシュボードにwebhook URLを設定します。(詳しくは、[こちら](https://qiita.com/katsuki_ono/items/df8830c8b07e3a59f354)を参考にしてください。)

※稀にエラーが発生します。エラーが発生した場合、エラー処理して頂くか、インスタンスを再起動してください。

webhook URLを設定後、LINE Developersで検証を押します。
成功と表示されたら、上手く接続出来ています。(インスタンス側では、上手くいっている場合は200と返されます。500の場合、プログラムに問題ありなどなので、普通のエラーと同じです。)

ここまで出来たら、ツールをテストします。
まず、LINE  botを友達登録してテキストを送信します。
ChatGPTが発声練習用のテキストをLINE botから返してきたら、LINEのテキスト入力の右横のボタンを押して、音声入力のボタンを押しながら録音します。
削除機能をテストするために削除と入力します。
上記、実行後、botから以下の画像のような返答があり、インスタンスにデータがあれば、成功です！
※ユーザーが例外的な操作を行った場合、LINEにエラーを返します。
※データ名は、ユーザーID.メッセージID.時間としています。
![IMG_8225.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3287642/0cd7597d-110d-e8f7-282a-2dd5663513dc.jpeg)

# 最後に
ChatGPTを使うと生活が豊かになっていくのを感じますね。
何年かかるかわかりませんが、データが溜まってきたら、音声認識AIをファインチューニングしてみたいなと思っています。
また何か作ってみます。