新幹線の駅がランダムに選ばれるJRさんのどこかにビューーン!で長野のワイナリーを巡りました。
とっても楽しかったので、日本のワイナリーがランダムで選ばれるワイン版のビューーン!を作ってみました。
今回開発したアプリケーションはLINE上で動作し、OpenAIのGPT-4を組み込んだワインでビューーン!のチャットボットとコミュニケーションができます。ユーザーから送信されたメッセージに「ワイン」や「ワイナリー」という単語があれば、ランダムで選ばれたワイナリーの情報が送信されます。同時に、選ばれたワイナリーへ新幹線で旅行をするイメージをDALL-Eによって描画し、その画像も一緒にお届けするシステムとなっています。
使用した技術は、Node.js / LINE Messaging API / OpenAI API(GPT-4,DALL-E), obnizです。obnizはこの企画のプレゼンテーション用に実装したおまけで、全てのプログラムが順調に作動すると、プラレールのはやぶさが発車します(祝!)
今回、OpenAIのAPIを使ってみて感動したポイントが2つ: Function Calling / DALL-Eのprompt あるので、そちらを重点的に紹介します!
- Function Calling
ワイン・ワイナリーなどのワインに関する単語があれば、日本国内のワイナリーリストからランダムに選ばれた2つのワイナリーの名称とURLを返すという関数を実行するプログラムです。
参考文献:
https://qiita.com/n0bisuke/items/b31fafe2239abf01b13a
https://qiita.com/n0bisuke/items/ad703ea959063b568cd7
わたしは普段UIUXデザイナーとしてお仕事をしているため、世の中を色と形によって認識することが多いです。なので、論理式で指示を書くコーディングは正直なところ得意ではなく、今でも苦手意識があります。でもこのFCは自然言語によって関数実行の指示を書くので、プログラミングとの距離が縮まって仲良くなれた感じがしています!
2.DALL-Eのprompt
ワイナリーの名前を受け取り、OpenAIの画像生成APIであるDALL-Eを呼び出して、名前から画像を生成するための関数を作成しています。ここで使用したpromptは下記の通りで、ワイナリーの名前がプロンプトの中に組み込まれています。これによって、その名称から想像された旅の情景が描かれるので、もう楽しすぎてずっと生成していました笑。
海外のお友達が、「日本のShinkansenってすごいよね」って英語で話していたことを思い出して、新幹線はそのまま英語表記でプロンプトにしてみたんですが、ちゃんと描かれていました!
参考文献:
https://qiita.com/yoyoda/items/a555abf9de9c36935145
https://zenn.dev/n0bisuke/scraps/0243ba9ab50ba5
3.axiosでAPIを叩く
この技術も今回初めて使ってみたのですが、感動的な体験のひとつでした。ワイナリーリストのAPIを叩いて、いくつ目のカラムの情報を取ってきてと指定すると、その通り実行してくれます。日本ワイナリーの情報をまとめてくださっている貴重なAPIにも感謝です!
参考文献:
https://zenn.dev/protoout/books/public-apis-api-get/viewer/01
https://uedayou.net/ldapinavi/linkdata/rdf1s4263i/Japanese_wine
【コード全文を紹介】
// 必要な環境設定
const axios = require('axios');
const express = require('express');
const line = require('@line/bot-sdk');
const PORT = process.env.PORT || 3000;
const Obniz = require('obniz');
// obnizID 設定
const obniz = new Obniz('YOURS');
// LINE massaging API キー設定
const config = {
channelSecret: 'YOURS',
channelAccessToken: 'YOURS'
};
const client = new line.Client(config);
const app = express();
// openai ライブラリを読み込む
const OpenAI = require('openai');
// 環境変数 OPENAI_API_KEY 設定
const openai = new OpenAI({
apiKey: 'YOURS',
});
// obniz おまけ実装
obniz.onconnect = async () => {
obniz.display.clear();
obniz.display.print('Wine de viewwn!');
console.log('ターミナル成功表示');
var motor = obniz.wired("DCMotor", {forward:0, back:1}); // モーター配線 導線赤が0番
app.get('/', (req, res) => res.send('Hello LINE BOT!(GET)')); //ブラウザ確認用(無くても問題ない)
app.post('/webhook', line.middleware(config), (req, res) => {
Promise
.all(req.body.events.map(handleEvent))
.then((result) => res.json(result))
.catch((err) => {
console.error(err);
res.status(500).end();
});
});
// 1. getWineryInfo関数
// 指定されたURLから日本ワイナリーのデータを取得し、ランダムに2つのワイナリー名とURLを返す
async function getWineryInfo() {
// 1.1 URLを定義します。
const URL = `http://linkdata.org/api/1/rdf1s4263i/Japanese_wine_tsv_uri.txt`;
// 1.2 axios.getを使用して、そのURLからデータを非同期で取得します。
const res = await axios.get(URL);
// 1.3 取得したデータを改行('\n')で分割して、各行を要素とする配列を生成します。
const lines = res.data.split('\n');
// 1.4 配列の最初の要素(おそらくヘッダー)を削除します。
lines.shift();
// 1.5 ランダムにワイナリー情報を選択し、その名称とURLをメッセージとして格納します。
let messages = []; // ワイナリー名とURL
let imageUrls = []; // 生成画像
if (lines.length >= 2) {
for (let i = 0; i < 2; i++) {
const randomIndex = Math.floor(Math.random() * lines.length);
const randomLine = lines[randomIndex];
const parts = randomLine.split('\t');
const URL = parts[5];
const WineryName = parts[2];
let individualMessage = `${i + 1} :\n${WineryName}\n${URL}\n\n`;
messages.push(individualMessage);
// 1.6 選択したワイナリー情報を元の配列から削除します。(選択されるワイナリーの重複を避ける)
lines.splice(randomIndex, 1);
}
messages.push('候補はこちらです!');
} else {
return ['データが不足しています。'];
}
return messages;
}
// 1. END
// 2.functionMappingsオブジェクト
// getWineryInfo関数の名前をキーとして、その関数自体を値として持つオブジェクト。のちに関数を呼び出すために定義
const functionMappings = {
getWineryInfo: getWineryInfo
};
// 2.END
// 3.generateImageUrl関数
// ワイナリーの名前を受け取り、それを基に画像を生成するためのプロンプトを作成
// OpenAIの画像生成APIを呼び出して、生成された画像のURLを返します。
// 3.1 WineryNameを使用して、画像生成のプロンプトを定義します。
const generateImageUrl = async (WineryName) => {
const PromptWineryName = `vivid pastel color dreamy illustration of shinkansen and wineries: ${WineryName.join(' and ')}.`;
const res = await openai.images.generate({
prompt: PromptWineryName,
n: 1,
size: "1024x1024"
});
// 3.2 生成された画像のURLを返します
return res.data[0].url;
}
// 4.handleEvent関数
// この関数は、入力されたイベントに基づいて適切なレスポンスを生成します。
async function handleEvent(event) {
if (event.type !== 'message' || event.message.type !== 'text') {
return Promise.resolve(null);
}
const { userId } = event.source;
const PromptFromUser = {
role: 'user',
content: event.message.text
};
// 4.1 gptOptionsの定義
// gptOptionsを定義します。これは、OpenAIのモデルにチャットの完成をリクエストするためのパラメータを含むオブジェクトです
const gptOptions = {
model: "gpt-4-0613",
messages: [PromptFromUser],
function_call: "auto",
functions: [
{
name: "getWineryInfo",
description: "ワイン・ワイナリーなどのワインに関する単語があれば、function_callを実行します。それによって、日本国内のワイナリーリストからランダムに選ばれた2つのワイナリーの名称とURLを返します",
parameters: {
type: "object",
properties: {}
},
},
],
};
// 4.6 OpenAI APIの呼び出し
// OpenAI APIを呼び出して、チャットの完了をリクエストします
const completion = await openai.chat.completions.create(gptOptions);
const message = completion.choices[0].message;
// ここで2つのワイナリーの情報を取得し、それぞれの画像URLを生成
const wineryInfo = await getWineryInfo();
const imageUrls = [];
for (let i = 0; i < wineryInfo.length - 1; i++) {
const name = wineryInfo[i].split('\n')[1]; // ワイナリーの名前を取得
const imageUrl = await generateImageUrl([name]);
imageUrls.push(imageUrl);
}
// 4.3 レスポンスメッセージの処理
// 関数呼び出しの結果である場合、対応する関数を実行し、その結果をレスポンスメッセージとして使用します テスト中
let responseMessages = [];
if (completion.choices[0].finish_reason === "function_call") {
responseMessages.push({
type: 'text',
text: 'ワインでビューーン!日本でおすすめのワイナリー情報&新幹線でそのワイナリーに旅をするイメージをお届けします'
});
for (let i = 0; i < wineryInfo.length - 1; i++) {
// テキストメッセージを追加
responseMessages.push({
type: 'text',
text: wineryInfo[i]
});
// 画像メッセージを追加
responseMessages.push({
type: 'image',
originalContentUrl: imageUrls[i],
previewImageUrl: imageUrls[i]
});
}
// obniz
motor.forward();
motor.power(50);//モーターのパワー制御 1~100
motor.move(true);
setTimeout(function(){
motor.stop();
}, 5000); //モーターが止まるまでの時間
} else {
responseMessages.push({
type: 'text',
text: message?.content
});
}
return client.replyMessage(event.replyToken, responseMessages);
} // 4.END
app.listen(PORT, () => {
console.log(`Server running at ${PORT}`);
});
}
今回の一連の制作はデジタルハリウッド大学大学院のプロダクト・プロトタイピングAという講義を通じて行いました。https://twitter.com/n0bisuke 先生の機智に満ちた講義やクラスメイトのものづくりの情熱に沢山の刺激を受け、私も制作をとことん楽しめました。これからも引き続きワインでビューーン!をアップデートして参ります。