LoginSignup
13

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

Last updated at Posted at 2023-03-25

はじめに

本記事では、OpenAI の ChatGPT API と Whisper API を使って、以下のような LINE のチャットボットを作成します。

  • ボットに人格設定可能
  • 音声入力にも対応
  • スタンプの意味も解釈して返答可能

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

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

追記

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

1. LINE bot の作成

1-1. LINE Developers に登録する

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

1-2. プロバイダーを作成する

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

スクリーンショット 2023-03-26 2.45.43.png

1-3. チャネルを作成する

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

スクリーンショット 2023-03-26 2.47.20.png

1-4. 作成されたアカウントを確認する

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

1-5. チャネルアクセストークンを取得する

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

スクリーンショット 2023-03-26 2.53.42.png

2. OpenAI の APIキーを取得する

2-1. OpenAIのアカウントを作成する

以下から作成してください。

2-2. 無料利用枠があるかどうか確認する

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

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

スクリーンショット 2023-03-26 3.00.32.png

2-3. APIキーを発行する

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

3. GoogleAppsScript を準備する

3-1. 新しいスプレッドシートを準備する

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

スクリーンショット 2023-03-26 3.09.56.png

3-2. キャラクターの設定を記載する

チャットボットのキャラクターを設定します。
キャラクターの設定を、settingsシートのA1セルに入力します。

スクリーンショット 2023-03-26 5.16.42.png

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

3-3. セルに名前をつける

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

スクリーンショット 2023-03-26 3.11.45.png

3-4. ログ用のシートに見出しをつける

logシートの1行目に、見出しをつけていきます。
以下の各文字列を、A1I1セルにそれぞれコピペしてください。
timestamp userId messageType userMessage botMessage promptTokens completionTokens totalTokens errorMessage

スクリーンショット 2023-03-26 5.04.58.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. コードをコピペする

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

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)を押します。
このプロセスをすっ飛ばすとエラーになるので、要注意です。
実行結果はエラーになると思いますが、気にしなくて大丈夫です。

3-8. ウェブアプリをデプロイする

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

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

4. LINEbotの設定をする

4-1. WebhookURLを設定する

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

4-2. Webhookの利用をオンにする

その真下のWebhookの利用のトグルスイッチをONにします。

4-3. 応答メッセージをオフにする

その少し下のLINE公式アカウント機能内の応答メッセージをOFFにします

5. テストする

5-1. LINEbotを友達追加する

アカウント管理画面を開き、今回作成したbotを選択します。

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

5-2. キャラクター設定を調整する

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

あとがき

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

追記

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13