お店で自分の青春時代の曲が流れたら妙にテンション上がりません?
若い人には伝わりにくいかもですが、どこかお店に入った時なんかに自分の青春時代ドンピシャの曲が流れてくるとテンション上がるって経験した方って結構多いのではないでしょうか?
今回何かを買う前に、「買いたいもの」と自分の年代を入れると、年代に合うヒット曲を提案してくれるBotを作りました。
これで買い物のテンションもだだ上がりです。
コードは冗長なところがあり読みづらいと思いますが、ChatGPTでLINE Botを作りたい、YoutubeAPIを用いて欲しい動画をLINEで受け取りたいなどを考えている方はぜひ読んでいただければと思います。
デモ動画はこちら
https://twitter.com/ped_yi/status/1667429700170579970
使用したもの
- Google Apps Script
- LINE Messaging API
- ChatGPT API
- Youtube API
-
リッチメニュー エディター
LNEBot✖️GASについてはこちらを参照してください。
リッチメニューの準備
こんな感じで作ってそれぞれの領域に該当するメッセージアクションを設定します。
スプレッドシートの中身
準備段階では1行目だけ買いておけばOKです。後はユーザーからの入力があれば更新されていきます。
ChatGPT APIを用いる
下記のコードでChatGPT APIを利用してresponseを受け取ることができます。
function callOpenAI(item, generation, userRow) {
const url = "https://api.openai.com/v1/chat/completions";
const content = "プロンプトをここに記載"
const options = {
method: "post",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${OPEN_AI_APY_KEY}`,
},
payload: JSON.stringify({
model: "gpt-3.5-turbo",
messages: [{ role: "user", content: content }],
}),
};
const response = UrlFetchApp.fetch(url, options);
const result = JSON.parse(response.getContentText());
const text = result.choices[0].message.content;
const modtext = JSON.parse(text.replace(/""/g, "'"));
for (let i = 0; i < 5; i++) {
num = 3 * i + 5;
mainsheet.getRange(userRow, num).setValue(modtext[i].description);
mainsheet.getRange(userRow, num + 1).setValue(modtext[i].title);
mainsheet.getRange(userRow, num + 2).setValue(modtext[i].artist);
}
}
text = result.choices[0].message.contentが文字列だったため、JSON.parseして各要素を取り出せるようにしました。
プロンプト
返信の型を指定してその後処理しやすいようにします。今回はリスト型を指定しました。
const content = `
今日は${item}を買いたい気分です。
私は${generation}流行った曲を聴きながら買い物を楽しみたいです。
邦楽のその時代のヒット曲の中からおすすめを5曲挙げて、それについての説明(description),曲名(title)とそのアーティスト名(artist)をjavascriptで使用できるリストで返してください。
単に流行った曲ではなく、${item}を意識しながら選んでください。必ず実在する邦楽でお願いします。
返事の形はリスト形で、
[{"description": , "title": , "artist": },{"description": , "title": , "artist":},{"description": , "title": , "artist":},{"description
": , "title": , "artist":},{"description":, "title": , "artist":}
]でお願いします。"artist"の要素も""で囲んでください。
# 条件
- 簡潔に
- 文字数:8000字まで
`
変数であるitem, generationの情報はスプレッドシートから抽出します。
ちなみにgenerationはユーザーが「青春」を選ぶと「(年齢から16引いた数)年前」、「最近」を選ぶと「最近」が代入されます。
そこのコードは下記の通り。
if (usermessage == "青春") {
age = mainsheet.getRange(userRow, 2).getValue();
mainsheet.getRange(userRow, 3).setValue(age - 16 + "年前");
messages = [
{
type: "text",
text:
"何を買いたい気分ですか? 入力してくれたらおすすめの曲を教えるよ!",
},
];
} else if (usermessage == "最近") {
mainsheet.getRange(userRow, 3).setValue("最近");
messages = [
{
type: "text",
text:
"何を買いたい気分ですか? 入力してくれたらおすすめの曲を教えるよ!",
},
];
}
おすすめ曲をYoutubeで提示する(カルーセルメッセージ)
こちらの記事を参考にしました。
- 上の記事を参考にしてGoogle Cloud PlatformからYoutubeDataAPIを有効にする。
- 指定の検索ワードによる検索結果で1番に出てくる動画のURLを取得する関数を作る(getFirstYouTubeVideoURL)
- カルーセルメッセージに入れる
getFirstYouTubeVideoURLの詳細は下記の通り。
function getFirstYouTubeVideoURL(searchTitle) {
var searchQuery = "site:youtube.com " + searchTitle;
var searchResults = [];
var results = YouTube.Search.list("id", { q: searchQuery, maxResults: 1 });
for (var i = 0; i < results.items.length; i++) {
var item = results.items[i];
var videoURL = "https://www.youtube.com/watch?v=" + item.id.videoId;
searchResults.push(videoURL);
}
return searchResults[0];
}
カルーセルメッセージの例は下記の通り。
その他のメッセージタイプにする場合は公式HPから。
messages = [
{
type: "template",
altText: "this is a carousel template",
template: {
type: "carousel",
columns: [
{
imageBackgroundColor: "#FFFFFF",
title: searchTitle_1,
text: searchTitle_1,
defaultAction: {
type: "uri",
label: "聴く",
uri: url_1,
},
actions: [
{
type: "uri",
label: "聴く",
uri: url_1,
},
],
}]
}
}
]
コード全体
const LINE_CHANNEL_ACCESS_TOKEN =
PropertiesService.getScriptProperties().getProperty(
"LINE_CHANNEL_ACCESS_TOKENをここに入れる"
) ||
"チャンネルアクセストークン";
const OPEN_AI_APY_KEY =
PropertiesService.getScriptProperties().getProperty("OPEN_AI_APY_KEY") ||
"OpenAIkeyをここに入れる";
const logsheet = SpreadsheetApp.openById(
""
).getSheetByName("log");
const mainsheet = SpreadsheetApp.openById(
""
).getSheetByName("main");
let messages;
function callOpenAI(item, generation, userRow) {
const url = "https://api.openai.com/v1/chat/completions";
const content = `
今日は${item}を買いたい気分です。
私は${generation}流行った曲を聴きながら買い物を楽しみたいです。
邦楽のその時代のヒット曲の中からおすすめを5曲挙げて、それについての説明(description),曲名(title)とそのアーティスト名(artist)をjavascriptで使用できるリストで返してください。
単に流行った曲ではなく、${item}を意識しながら選んでください。必ず実在する邦楽でお願いします。
返事の形はリスト形で、
[{"description": , "title": , "artist": },{"description": , "title": , "artist":},{"description": , "title": , "artist":},{"description
": , "title": , "artist":},{"description":, "title": , "artist":}
]でお願いします。"artist"の要素も""で囲んでください。
# 条件
- 簡潔に
- 文字数:8000字まで
`;
const options = {
method: "post",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${OPEN_AI_APY_KEY}`,
},
payload: JSON.stringify({
model: "gpt-3.5-turbo",
messages: [{ role: "user", content: content }],
}),
};
const response = UrlFetchApp.fetch(url, options);
const result = JSON.parse(response.getContentText());
const text = result.choices[0].message.content;
const modtext = JSON.parse(text.replace(/""/g, "'"));
for (let i = 0; i < 5; i++) {
num = 3 * i + 5;
mainsheet.getRange(userRow, num).setValue(modtext[i].description);
mainsheet.getRange(userRow, num + 1).setValue(modtext[i].title);
mainsheet.getRange(userRow, num + 2).setValue(modtext[i].artist);
}
}
function doPost(e) {
var json = JSON.parse(e.postData.contents);
var reply_token = json.events[0].replyToken;
if (typeof reply_token === "undefined") {
return;
}
if (json.events[0].type == "message") {
const usermessage = json.events[0].message.text;
const user_id = json.events[0].source.userId;
var userRow = searchrow(user_id);
if (usermessage == "20-25") {
mainsheet.getRange(userRow, 2).setValue(23);
messages = [
{
type: "text",
text: "年代を選んでください",
quickReply: {
items: [
{
type: "action",
action: {
type: "message",
label: "青春",
text: "青春",
},
},
{
type: "action",
action: {
type: "message",
label: "最近",
text: "最近",
},
},
],
},
},
];
} else if (usermessage == "25-30") {
mainsheet.getRange(userRow, 2).setValue(28);
messages = [
{
type: "text",
text: "年代を選んでください",
quickReply: {
items: [
{
type: "action",
action: {
type: "message",
label: "青春",
text: "青春",
},
},
{
type: "action",
action: {
type: "message",
label: "最近",
text: "最近",
},
},
],
},
},
];
} else if (usermessage == "30-35") {
mainsheet.getRange(userRow, 2).setValue(33);
messages = [
{
type: "text",
text: "年代を選んでください",
quickReply: {
items: [
{
type: "action",
action: {
type: "message",
label: "青春",
text: "青春",
},
},
{
type: "action",
action: {
type: "message",
label: "最近",
text: "最近",
},
},
],
},
},
];
} else if (usermessage == "35-40") {
mainsheet.getRange(userRow, 2).setValue(38);
messages = [
{
type: "text",
text: "年代を選んでください",
quickReply: {
items: [
{
type: "action",
action: {
type: "message",
label: "青春",
text: "青春",
},
},
{
type: "action",
action: {
type: "message",
label: "最近",
text: "最近",
},
},
],
},
},
];
} else if (usermessage == "40-45") {
mainsheet.getRange(userRow, 2).setValue(43);
messages = [
{
type: "text",
text: "年代を選んでください",
quickReply: {
items: [
{
type: "action",
action: {
type: "message",
label: "青春",
text: "青春",
},
},
{
type: "action",
action: {
type: "message",
label: "最近",
text: "最近",
},
},
],
},
},
];
} else if (usermessage == "45-50") {
mainsheet.getRange(userRow, 2).setValue(48);
messages = [
{
type: "text",
text: "年代を選んでください",
quickReply: {
items: [
{
type: "action",
action: {
type: "message",
label: "青春",
text: "青春",
},
},
{
type: "action",
action: {
type: "message",
label: "最近",
text: "最近",
},
},
],
},
},
];
} else if (usermessage == "Youtubeで曲を聴く") {
searchTitle_1 =
mainsheet.getRange(userRow, 6).getValue() +
" " +
mainsheet.getRange(userRow, 7).getValue();
searchTitle_2 =
mainsheet.getRange(userRow, 9).getValue() +
" " +
mainsheet.getRange(userRow, 10).getValue();
searchTitle_3 =
mainsheet.getRange(userRow, 12).getValue() +
" " +
mainsheet.getRange(userRow, 13).getValue();
searchTitle_4 =
mainsheet.getRange(userRow, 15).getValue() +
" " +
mainsheet.getRange(userRow, 16).getValue();
searchTitle_5 =
mainsheet.getRange(userRow, 18).getValue() +
" " +
mainsheet.getRange(userRow, 19).getValue();
url_1 = getFirstYouTubeVideoURL(searchTitle_1);
url_2 = getFirstYouTubeVideoURL(searchTitle_2);
url_3 = getFirstYouTubeVideoURL(searchTitle_3);
url_4 = getFirstYouTubeVideoURL(searchTitle_4);
url_5 = getFirstYouTubeVideoURL(searchTitle_5);
messages = [
{
type: "template",
altText: "this is a carousel template",
template: {
type: "carousel",
columns: [
{
// thumbnailImageUrl:
// "https://yuiueda1218.github.io/audiourl/kimetsu.png",
imageBackgroundColor: "#FFFFFF",
title: searchTitle_1,
text: searchTitle_1,
defaultAction: {
type: "uri",
label: "聴く",
uri: url_1,
},
actions: [
{
type: "uri",
label: "聴く",
uri: url_1,
},
],
},
{
// thumbnailImageUrl:
// "https://yuiueda1218.github.io/audiourl/kimetsu.png",
imageBackgroundColor: "#FFFFFF",
title: searchTitle_2,
text: searchTitle_2,
defaultAction: {
type: "uri",
label: "聴く",
uri: url_2,
},
actions: [
{
type: "uri",
label: "聴く",
uri: url_2,
},
],
},
{
// thumbnailImageUrl:
// "https://yuiueda1218.github.io/audiourl/kimetsu.png",
imageBackgroundColor: "#FFFFFF",
title: searchTitle_3,
text: searchTitle_3,
defaultAction: {
type: "uri",
label: "聴く",
uri: url_3,
},
actions: [
{
type: "uri",
label: "聴く",
uri: url_3,
},
],
},
{
// thumbnailImageUrl:
// "https://yuiueda1218.github.io/audiourl/kimetsu.png",
imageBackgroundColor: "#FFFFFF",
title: searchTitle_4,
text: searchTitle_4,
defaultAction: {
type: "uri",
label: "聴く",
uri: url_4,
},
actions: [
{
type: "uri",
label: "聴く",
uri: url_4,
},
],
},
{
// thumbnailImageUrl:
// "https://yuiueda1218.github.io/audiourl/kimetsu.png",
imageBackgroundColor: "#FFFFFF",
title: searchTitle_5,
text: searchTitle_5,
defaultAction: {
type: "uri",
label: "聴く",
uri: url_5,
},
actions: [
{
type: "uri",
label: "聴く",
uri: url_5,
},
],
},
],
},
},
];
} else if (usermessage == "青春") {
age = mainsheet.getRange(userRow, 2).getValue();
mainsheet.getRange(userRow, 3).setValue(age - 16 + "年前");
messages = [
{
type: "text",
text:
"何を買いたい気分ですか? 入力してくれたらおすすめの曲を教えるよ!",
},
];
} else if (usermessage == "最近") {
mainsheet.getRange(userRow, 3).setValue("最近");
messages = [
{
type: "text",
text:
"何を買いたい気分ですか? 入力してくれたらおすすめの曲を教えるよ!",
},
];
} else {
mainsheet.getRange(userRow, 4).setValue(usermessage);
const item = mainsheet.getRange(userRow, 4).getValue();
const generation = mainsheet.getRange(userRow, 3).getValue();
callOpenAI(item, generation, userRow);
messages = [
{
type: "text",
text:
"1曲目!\n" +
mainsheet.getRange(userRow, 5).getValue() +
"\n" +
mainsheet.getRange(userRow, 7).getValue() +
" " +
mainsheet.getRange(userRow, 6).getValue(),
},
{
type: "text",
text:
"2曲目!\n" +
mainsheet.getRange(userRow, 8).getValue() +
"\n" +
mainsheet.getRange(userRow, 10).getValue() +
" " +
mainsheet.getRange(userRow, 9).getValue(),
},
{
type: "text",
text:
"3曲目!\n" +
mainsheet.getRange(userRow, 11).getValue() +
"\n" +
mainsheet.getRange(userRow, 13).getValue() +
" " +
mainsheet.getRange(userRow, 12).getValue(),
},
{
type: "text",
text:
"4曲目!\n" +
mainsheet.getRange(userRow, 14).getValue() +
"\n" +
mainsheet.getRange(userRow, 16).getValue() +
" " +
mainsheet.getRange(userRow, 15).getValue(),
},
{
type: "text",
text:
"5曲目!\n" +
mainsheet.getRange(userRow, 17).getValue() +
"\n" +
mainsheet.getRange(userRow, 19).getValue() +
" " +
mainsheet.getRange(userRow, 18).getValue(),
},
];
}
replyToLine(reply_token, messages);
}
return ContentService.createTextOutput(
JSON.stringify({ content: "post ok" })
).setMimeType(ContentService.MimeType.JSON);
}
function replyToLine(replyToken, messages) {
const url = "https://api.line.me/v2/bot/message/reply";
const options = {
method: "post",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${LINE_CHANNEL_ACCESS_TOKEN}`,
},
payload: JSON.stringify({
replyToken: replyToken,
messages: messages,
}),
};
UrlFetchApp.fetch(url, options);
}
function getFirstYouTubeVideoURL(searchTitle) {
var searchQuery = "site:youtube.com " + searchTitle;
var searchResults = [];
var results = YouTube.Search.list("id", { q: searchQuery, maxResults: 1 });
for (var i = 0; i < results.items.length; i++) {
var item = results.items[i];
var videoURL = "https://www.youtube.com/watch?v=" + item.id.videoId;
searchResults.push(videoURL);
}
return searchResults[0];
}
function searchrow(user_id) {
for (var i = 1; i <= 1000; i++) {
if (mainsheet.getRange(i, 1).getValue() == user_id) {
return i;
}
}
lastRow =
mainsheet
.getLastRow()+ 1;
mainsheet.getRange(lastRow, 1).setValue(user_id);
return lastRow;
}
おわりに
今回はリテールテックハッカソンというハッカソンで6時間くらいで作るものだったので、これが限界でした。
あと、ChatGPTからの結果がアーティストとタイトルが間違っていたりして、YoutubeのURLが全然違うものが出ることもたまにありましたが、そこは御愛嬌ということで・・・笑。