【Whisper APIで音声入力にも対応！】ChatGPT x GoogleAppsScript(GAS) でお手軽LINEbotを作ってみた

# はじめに
本記事では、OpenAI の ChatGPT API と Whisper API を使って、以下のような LINE のチャットボットを作成します。
- ボットに人格設定可能
- 音声入力にも対応
- スタンプの意味も解釈して返答可能

LINEbot を作ったことがない方はもちろん、プログラミング経験がほとんどない方でも実装できる方法をご紹介します！

なお、本記事は以下の記事の続編です。こちらもよろしければご覧ください。

https://qiita.com/CurryPrince/items/00aa5052d858707b5d82

# 追記
文脈理解（履歴の記憶）については以下の記事で解説しています。
こちらもあわせてぜひご確認ください（以下の記事は、本記事の続編です）。

https://qiita.com/CurryPrince/items/15037e97091456fbcaf3

# 1. LINE bot の作成
## 1-1. LINE Developers に登録する
以下のサイトにアクセスし、Developer登録をしてください。ログインは普段お使いのLINEアカウントで大丈夫です。

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

## 1-2. プロバイダーを作成する
以下のプロバイダー作成画面から、新規プロバイダーを作成してください。
プロバイダーというのは会社名のようなものだと思ってください。複数のbotを作成するような場合は、1つのプロバイダーに複数のbotがぶら下がるイメージです。

https://developers.line.biz/console/

![スクリーンショット 2023-03-26 2.45.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2956515/1fd04d8f-d06b-4b1d-f884-4eb5ddb992e9.png)

## 1-3. チャネルを作成する
作成したプロバイダーを選択し、画面内の`新規チャネル作成`を選択してください。
チャネルの種類は`Messaging API`を選択してください。
また、ここで設定した`チャネルアイコン`と`チャネル名`が、そのままbotのアイコンとアカウント名になるので、ご注意ください。

![スクリーンショット 2023-03-26 2.47.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2956515/1e03165b-1021-aa72-907c-7748930fd121.png)

## 1-4. 作成されたアカウントを確認する
以下のアカウント管理画面に、上記で作成したアカウントがあることを確認してください。

https://manager.line.biz/

## 1-5. チャネルアクセストークンを取得する
LINE Developers の画面にもどり、上記で作成したプロバイダー、チャネルを選択します。
その中の`Messaging API設定`というタブの最下部にある`チャネルアクセストークン`というところで`発行`をクリックします。すると長いトークン（文字列）が発行されるので、これを控えておきます。

![スクリーンショット 2023-03-26 2.53.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2956515/ccaecd88-65b2-ba11-96b2-e170d8507541.png)

# 2. OpenAI の APIキーを取得する
## 2-1. OpenAIのアカウントを作成する
以下から作成してください。

https://beta.openai.com/signup/

## 2-2. 無料利用枠があるかどうか確認する
上記で作成したアカウントにログインした状態で、以下から無料枠があるかどうか確認してください。

https://platform.openai.com/account/usage

`Free trial usage`があればOKです。
2023年3月時点では、アカウント新規作成から3ヶ月間は毎月18ドルの無料枠があるのですが、もしこの提供が終了してしまった場合は、クレカの登録等が必要になってきます。
また、<b>「1つの連絡先に紐づく無料枠は1つまで」という制約があり、たとえば個人アカウントを作った後に会社アカウントを作成し、それらのアカウントが同じ電話番号で紐づいていた場合などは、後から作った会社アカウントの方には無料枠がつかないのでご注意ください</b>（地味にハマりやすいポイントです）。

![スクリーンショット 2023-03-26 3.00.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2956515/ea57c92c-821b-0f5b-b214-52a6f87d9f33.png)

## 2-3. APIキーを発行する
以下の画面にログインし、`+ Create new secret key`を選択してAPIキーを発行してください。
ここで発行したAPIキーは、すぐに控えないと後から見られないので注意してください。といっても、分からなくなったら新しいものを発行すれば良いので、あまり問題はないです。

https://platform.openai.com/account/api-keys

# 3. GoogleAppsScript を準備する
## 3-1. 新しいスプレッドシートを準備する
Googleスプレッドシートを新規作成し、`log`と`settings`という名前のシートを準備します。
`log`にはメッセージの履歴、`settings`にはbotのキャラクター設定を追加していきます。

![スクリーンショット 2023-03-26 3.09.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2956515/1e24c153-d72f-a39c-149e-d05f7c083b9f.png)

## 3-2. キャラクターの設定を記載する
チャットボットのキャラクターを設定します。
キャラクターの設定を、`settings`シートの`A1`セルに入力します。

![スクリーンショット 2023-03-26 5.16.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2956515/81231fc0-00f5-bee0-681e-eddcc38b11e9.png)

キャラクター設定についてはたくさんの方々が研究されているので、以下のような記事が大変参考になります！色々と試してみてください。

https://note.com/fladdict/n/neff2e9d52224

https://note.com/it_navi/n/nf5f702b36a75

https://chatgpt-lab.com/n/n28140f24debe

## 3-3. セルに名前をつける
`settings`シートの`A1`セルを選択した状態で、メニューの「名前付き範囲」から、このセルに`ai_settings`という名前をつけてください。以下の画像の通りになればOKです。

![スクリーンショット 2023-03-26 3.11.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2956515/d9ee041c-5a5c-0b68-c705-cb2c046a40a2.png)

## 3-4. ログ用のシートに見出しをつける
`log`シートの1行目に、見出しをつけていきます。
以下の各文字列を、`A1`〜`I1`セルにそれぞれコピペしてください。
`timestamp` `userId` `messageType` `userMessage` `botMessage` `promptTokens` `completionTokens` `totalTokens` `errorMessage`

![スクリーンショット 2023-03-26 5.04.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2956515/9a4e127e-0955-256f-7ffd-b48cf17eca70.png)

### 各項目の解説

|項目|解説|
|:---|:---|
|timestamp|LINEbotが返答した日時|
|userId|メッセージを返したユーザーのuserId|
|messageType|文字列なら`text`、スタンプなら`sticker`、音声なら`audio`など|
|userMessage|ユーザーから送られてきたメッセージ|
|botMessage|botが回答したメッセージ|
|promptTokens|ChatGPTのAPIに送ったメッセージのトークン数|
|completionTokens|ChatGPTのAPIから受信したメッセージのトークン数|
|totalTokens|合計のトークン数|
|errorMessage|エラー発生時のメッセージ|

## 3-5. スクリプトプロパティを設定する
スクリプトプロパティとは、各種パスワード等、コード内にベタ打ちしたくない機密性の高い情報を保存しておく場所です。
まずはスプレッドシートのメニューから`拡張機能` > `Apps Script`を選択します。すると、Google Apps Script のエディタ画面が開きます。

続いて、左メニューの歯車マーク`プロジェクトの設定`を選択すると、一番下に`スクリプト プロパティを編集`というボタンがあるので、それをクリック。以下の2つを設定します。

|プロパティ|値|
|:---|:---|
|LINE_ACCESS_TOKEN|1-5.で取得したLINEのチャネルアクセストークン|
|OPENAI_API_KEY|2-3.で取得した OpenAIのAPIキー|

## 3-6. コードをコピペする
左メニューの`<>`のマーク`エディタ`に戻り、最初から入力されているソースコードを削除した上で、以下のコードをコピペします。

```javascript:main.gs
//スプレッドシートからAIのキャラクター設定を取得する
function getAiSettings(){
  try{
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    const aiSettings = ss.getRangeByName("ai_settings").getValue();
    return aiSettings;
  } catch(e){
    return null;
  }
}

//promptの内容をキャラクター設定されたAI（ChatGPT API）に投げて回答を取得する
function getReply(prompt){
  const OPENAI_API_KEY = PropertiesService.getScriptProperties().getProperty("OPENAI_API_KEY");

  let messages = [{
    "role": "user",
    "content": prompt
  }];

  const characterSettings = getAiSettings();
  
  if(characterSettings != null){
    messages.unshift({
      "role": "system",
      "content": characterSettings
    });
  }

  const payload = {
    "model": "gpt-3.5-turbo",
    "temperature" : 0.5, //0〜1で設定。大きいほどランダム性が強い
    "max_tokens": 500, //LINEのメッセージ文字数制限が500文字なので、それに合わせて調整
    "messages": messages
  };
  
  const requestOptions = {
    "method": "post",
    "headers": {
      "Content-Type": "application/json",
      "Authorization": "Bearer "+ OPENAI_API_KEY
    },
    "payload": JSON.stringify(payload)
  };

  const response = UrlFetchApp.fetch("https://api.openai.com/v1/chat/completions", requestOptions);

  const responseText = response.getContentText();
  const json = JSON.parse(responseText);
  const retObj = {
    "payload": payload,
    "message": json.choices[0].message.content,
    "usage": json.usage
  };
  return retObj;
}

//file（音声ファイル）の内容をWhisperAPIを利用して文字列として取得する
function speechToText(file){
  const OPENAI_API_KEY = PropertiesService.getScriptProperties().getProperty("OPENAI_API_KEY");

  const payload = {
    "model": "whisper-1",
    "temperature" : 0,
    "language": "ja", //日本語以外にも対応する場合はこのプロパティは外す
    "file": file
  };
  
  const requestOptions = {
    "method": "post",
    "headers": {
      "Authorization": "Bearer "+ OPENAI_API_KEY
    },
    "payload": payload
  };
  try{
    const response = UrlFetchApp.fetch("https://api.openai.com/v1/audio/transcriptions", requestOptions);

    const responseText = response.getContentText();
    const json = JSON.parse(responseText);
    const text = json.text
    return text;
  } catch(e){
    return e.message;
  }
}

//LINEでユーザーから送られてきた音声ファイルを取得する
function getContentByUser(messageId){
  const LINE_ACCESS_TOKEN = PropertiesService.getScriptProperties().getProperty("LINE_ACCESS_TOKEN");
  const url = `https://api-data.line.me/v2/bot/message/${messageId}/content`;
  const requestOptions = {
    'headers': {
      'Authorization': 'Bearer ' + LINE_ACCESS_TOKEN,
    },
    'method': 'get'
  };
  const response = UrlFetchApp.fetch(url, requestOptions);
  return response.getBlob().setName(`${messageId}.m4a`); //拡張子を指定しないとWhisperAPI側でエラーになるので注意
}

//LINEでユーザーから送られてきたメッセージ（スタンプや音声含む）を文字列に変換する
function convertMessageObjToText(messageObj){
  let result = "";
  const messageType = messageObj.type;
  switch(messageType){
    case "text": //文字列
      result = messageObj.text;
      break;
    case "sticker": //スタンプ。キーワードが設定されていればそれを取得する
      if(messageObj.keywords === undefined){
        result = "？？？";
      } else{
        result = messageObj.keywords.join(",");
      }
      break;
    case "image": //画像
      result = "この画像が分かりますか？";
      break;
    case "video": //動画
      result = "この動画が分かりますか？";
      break;
    case "audio": //音声。文字起こしする
      if(messageObj.contentProvider.type === "line"){
        const audioFile = getContentByUser(messageObj.id);
        const transcriptedText = speechToText(audioFile);
        result = transcriptedText;
      } else{
        result = "この音声が聞こえますか？";
      }
      break;
    case "file": //ファイル
      result = "このファイルは見られますか？";
      break;
    case "location": //位置情報
      let locationInfo = messageObj.title ? messageObj.title + "\n" : "";
      locationInfo += messageObj.address ? messageObj.address + "\n" : "";
      locationInfo += `latitude:${messageObj.latitude}\n`;
      locationInfo += `longitude:${messageObj.longitude}`;
      result = "ここはどんな場所ですか？\n" + locationInfo;
      break;
    default: //その他
      result = "？？？";
  }
  return result;
}

//logシートにログを出力する
function appendLog(logArray){
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const logSheet = ss.getSheetByName("log");
  logSheet.appendRow(logArray);
}

//リクエストが送られるとこの関数が実行される
function doPost(e) {
  const LINE_ACCESS_TOKEN = PropertiesService.getScriptProperties().getProperty("LINE_ACCESS_TOKEN");
  const events = JSON.parse(e.postData.contents).events;
  const url = 'https://api.line.me/v2/bot/message/reply';

  const event = events[0];
  const replyToken = event.replyToken;
  const userMessage = convertMessageObjToText(event.message);
  const gptReply = getReply(userMessage);
  const botMessage = gptReply.message;
  const requestOptions = {
    'headers': {
      'Content-Type': 'application/json; charset=UTF-8',
      'Authorization': 'Bearer ' + LINE_ACCESS_TOKEN,
    },
    'method': 'post',
    'payload': JSON.stringify({
      'replyToken': replyToken,
      'messages': [{
        'type': 'text',
        'text': botMessage,
      }]
    })
  };

  const response = UrlFetchApp.fetch(url, requestOptions);
  
  const timestamp = Utilities.formatDate(new Date(), "Asia/Tokyo", "yyyy/MM/dd HH:mm:ss");
  const userId = event.source.userId;
  const messageType = event.message.type;
  let errorMessage = "";
  if(response !== {}){
    errorMessage = response.message;
  }
  const promptTokens = gptReply.usage.prompt_tokens;
  const completionTokens = gptReply.usage.completion_tokens;
  const totalTokens = gptReply.usage.total_tokens;

  appendLog([
    timestamp,
    userId,
    messageType,
    userMessage,
    botMessage,
    promptTokens,
    completionTokens,
    totalTokens,
    errorMessage
  ]);

  return ContentService.createTextOutput(JSON.stringify({"content": "success"})).setMimeType(ContentService.MimeType.JSON);
}
```

## 3-7. 必要な権限を付与する
エディタで一度`実行`ボタンを押しましょう。すると、いくつかの権限の許可確認画面が出てくるので、`許可（Accept）`を押します。
<b>このプロセスをすっ飛ばすとエラーになるので、要注意です。</b>
実行結果はエラーになると思いますが、気にしなくて大丈夫です。

## 3-8. ウェブアプリをデプロイする
エディタ右上の`デプロイ`ボタンを押し、
`新しいデプロイ` > `種類の選択` > `ウェブアプリ` を選択します。
また、`アクセスできるユーザー`を`全員`にした上で、右下の`デプロイ`ボタンを押します。

すると、URLが発行されるので、それを控えておきます。

# 4. LINEbotの設定をする
## 4-1. WebhookURLを設定する
LINE Developers に戻ります。
`作成したプロバイダー` > `作成したチャネル` > `Messaging API設定` に進み、`Webhook設定`の`Webhook URL`の`編集`ボタンを押し、3-8.で取得したURLを設定します。
その後、`検証`ボタンを押すと、疎通確認ができます。

https://developers.line.biz/console/

## 4-2. Webhookの利用をオンにする
その真下の`Webhookの利用`のトグルスイッチをONにします。

## 4-3. 応答メッセージをオフにする
その少し下の`LINE公式アカウント機能`内の`応答メッセージ`をOFFにします

# 5. テストする
## 5-1. LINEbotを友達追加する
アカウント管理画面を開き、今回作成したbotを選択します。

https://manager.line.biz/

その後、左メニュー内の`友だち追加ガイド`から、友達追加用のURLやQRコードが発行できるので、それを使用して自分のアカウントにbotを友達登録します。

## 5-2. キャラクター設定を調整する
LINEでbotとやり取りしながら、キャラクター設定を調整します。
スプレッドシートの`settings`シートの`A1`セルを編集すればすぐに反映されるので、好みのキャラクターができるまでカスタマイズしてみてください。
（キャラクター設定の変更後、再度デプロイする必要はありません）

# あとがき
Whisper APIをGASで使ったという事例が見つからなかったので、音声ファイルの取り扱いなどで地味に苦労しました。
Whisper APIは初めて使ってみましたが、性能がかなり高くて驚いています。ほぼ正確に文字起こししてくれます。
ここまでできたら、次は音声合成ソフトなどを使用して、会話できるbotも作ってみたいですね！

# 追記
文脈理解（履歴の記憶）については以下の記事で解説しています。
こちらもあわせてぜひご確認ください（以下の記事は、本記事の続編です）。

https://qiita.com/CurryPrince/items/15037e97091456fbcaf3