テレビ離れが叫ばれて久しい今日この頃
子どもの頃、テレビを見ることに全力を注ぎ、
兄弟姉妹でチャンネル権を争ってケンカしたり、
怒った親にテレビを禁じられて大泣きしたりしていた
そこのあなた!
「最近面白いテレビないなぁ」なんて、テレビから遠ざかってしまってはいないでしょうか。
一方で、今や多くの方が利用しているのがNetflixやAmazon Prime Videoなどのサブスク動画配信サービス。
サブスク動画配信サービスがテレビよりも優れている点は何か?
- いつでも好きな時に好きな番組が見れる
- 見れる作品数が多い
などがありますが、私はある観点に着目しました。
レコメンド機能がある
何事にもタイパが重視されるこの時代
忙しい私たちには、新たな作品を能動的に探している暇などないのです!!
実際私はAmazon Prime Videoを利用するとき、「あなたが興味のありそうな」カテゴリに表示されている動画から、新たなおもしろ作品に出会うことがほとんど。
したがって、普段見ているテレビ番組から、おすすめのテレビ番組を提案してくれるツールがあれば、テレビを楽しむ人が増えるのではないでしょうか。
テレビを楽しむ人が増えれば番組制作の予算も増え、面白い番組が増えるでしょう!
さあ、甦れ!!テレビの黄金時代
使用したもの
- Node-RED(Railwayでデプロイ)
- LINE Bot(Messaging API)
- NHK番組表API(Program Genre API(Ver.2))
- ChatGPT API
こんな感じの機能を作ろう(設計)
-
ユーザがLINE Botで好きな番組名を入力する
-
ChatGPTが入力された番組をジャンル分けする
- NHK番組表APIのリクエストには、4桁のジャンルIDを引数として設定する必要があります
NHK番組表APIリクエストhttps://api.nhk.or.jp/v2/pg/genre/{地域ID}/{サービスID(総合,Eテレetc)}/{ジャンルID}/{日付}.json?key={apikey}
-
NHK番組表APIを叩き、該当ジャンルの番組情報を取得する
-
取得した情報をいい感じに編集し、LINE Botに返す
※参考:ジャンルIDとジャンルの対応(NHK番組表API)
ジャンルID | ジャンル |
---|---|
0000 | ニュース |
0100 | スポーツ(スポーツニュース) |
0205 | 情報/ワイドショー(グルメ・料理) |
0300 | ドラマ(国内ドラマ) |
0409 | 音楽(童謡・キッズ) |
0502 | バラエティ(トークバラエティ) |
0600 | 映画(洋画) |
0700 | アニメ/特撮(国内アニメ) |
0800 | ドキュメンタリー/教養(社会・時事) |
0903 | 劇場/公演(落語・演芸) |
1000 | 趣味/教育(旅・釣り・アウトドア) |
1100 | 福祉(高齢者) |
機能を実装しよう
ノードに振った番号①~③の順に解説していきます!
(誰か教えてきれいにノードを配置する方法)
[{"id":"088145566f68e247","type":"tab","label":"フロー 4","disabled":false,"info":"","env":[]},{"id":"05197443aab6d823","type":"debug","z":"088145566f68e247","name":"デバッグ1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":670,"y":60,"wires":[]},{"id":"88932518a5be1a13","type":"function","z":"088145566f68e247","name":"②setGenreId","func":"const userText = msg.payload;\nlet genreId = '';\n\n// 0000: ニュース\nif (userText.indexOf('0000') > -1) {\n genreId = '0000';\n// 0100: スポーツ(スポーツニュース)\n} else if (userText.indexOf('0100') > -1) {\n genreId = '0100';\n// 0205: 情報 / ワイドショー(グルメ・料理)\n} else if (userText.indexOf('0205') > -1) {\n genreId = '0205';\n// 0300: ドラマ(国内ドラマ)\n} else if (userText.indexOf('0300') > -1) {\n genreId = '0300';\n// 0409: 音楽(童謡・キッズ)\n} else if (userText.indexOf('0409') > -1) {\n genreId = '0409';\n// 0502: バラエティ(トークバラエティ)\n} else if (userText.indexOf('0502') > -1) {\n genreId = '0502';\n// 0600: 映画(洋画)\n} else if (userText.indexOf('0600') > -1) {\n genreId = '0600';\n// 0700: アニメ / 特撮(国内アニメ)\n} else if (userText.indexOf('0700') > -1) {\n genreId = '0700';\n// 0800: ドキュメンタリー / 教養(社会・時事)\n} else if (userText.indexOf('0800') > -1) {\n genreId = '0800';\n// 0903: 劇場 / 公演(落語・演芸)\n} else if (userText.indexOf('0903') > -1) {\n genreId = '0903';\n// 1000: 趣味 / 教育(旅・釣り・アウトドア)\n} else if (userText.indexOf('1000') > -1) {\n genreId = '1000';\n// 1100: 福祉(高齢者)\n} else if (userText.indexOf('1100') > -1) {\n genreId = '1100';\n} else {\n genreId = '9999';\n}\n\n// 日付をYYYY-MM-DDの書式で返すメソッド\nfunction formatDate(dt) {\n dt.setHours(dt.getHours() + 9);\n var y = dt.getFullYear();\n var m = ('00' + (dt.getMonth() + 1)).slice(-2);\n var d = ('00' + dt.getDate()).slice(-2);\n return (y + '-' + m + '-' + d);\n}\n\nmsg.payload = {\n \"genreId\": genreId,\n \"date\": formatDate(new Date())\n};\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":110,"y":120,"wires":[["e47ded1cd7965421","57a71f4aec71c79d"]]},{"id":"e47ded1cd7965421","type":"debug","z":"088145566f68e247","name":"デバッグ2","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":670,"y":120,"wires":[]},{"id":"b334e2bf2d2da7e4","type":"debug","z":"088145566f68e247","name":"デバッグ3","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":670,"y":180,"wires":[]},{"id":"80eb97197b3ae0f0","type":"http request","z":"088145566f68e247","name":"③-B-1番組名である:番組表APIにリクエスト","method":"GET","ret":"obj","paytoqs":"ignore","url":"https://api.nhk.or.jp/v2/pg/genre/130/g1/{{payload.genreId}}/{{payload.date}}.json?key=[APIキーを入力]","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":320,"y":260,"wires":[["bf937e80adaac941","782b6edf78f7de01"]]},{"id":"bf937e80adaac941","type":"function","z":"088145566f68e247","name":"③-B-2:番組名である:replyProgram","func":"var count = Object.keys(msg.payload.list.g1).length;\nvar i = Math.floor((Math.random() * (count + 1)));\nvar programTitle = msg.payload.list.g1[i].title;\nvar programContent = msg.payload.list.g1[i].content;\nvar start_time = msg.payload.list.g1[i].start_time.split('T');\nvar start_date = start_time[0].split('-');\nvar start_hour = start_time[1].split(':');\nmsg.payload = 'あなたにおすすめの番組は・・・\\n「' \n + programTitle + '」\\n\\n' \n + programContent + '\\n\\n' \n + start_date[0] + '年' + start_date[1] + '月' + start_date[2] + '日'\n + start_hour[0] + '時' + start_hour[1] + '分スタート!';\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":430,"y":320,"wires":[["a2bec31e483b2ae4","b334e2bf2d2da7e4"]]},{"id":"650ce299cc0ac60f","type":"Webhook","z":"088145566f68e247","name":"","url":"/webhook","x":100,"y":60,"wires":[["6fcf7009580661ba"]]},{"id":"a2bec31e483b2ae4","type":"ReplyMessage","z":"088145566f68e247","name":"","replyMessage":"","x":680,"y":320,"wires":[]},{"id":"6fcf7009580661ba","type":"simple-chatgpt","z":"088145566f68e247","name":"①ChatGPT APIにリクエスト","Token":"Tokenを入力","SystemSetting":"以下のジャンルに分類してください。「ID: ジャンル」となっているので、IDの4桁の数値のみ回答お願いします。 0000: ニュース 0100: スポーツ(スポーツニュース) 0205: 情報/ワイドショー(グルメ・料理) 0300: ドラマ(国内ドラマ) 0409: 音楽(童謡・キッズ) 0502: バラエティ(トークバラエティ) 0600: 映画(洋画) 0700: アニメ/特撮(国内アニメ) 0800: ドキュメンタリー/教養(社会・時事) 0903: 劇場/公演(落語・演芸) 1000: 趣味/教育(旅・釣り・アウトドア) 1100: 福祉(高齢者)","x":300,"y":60,"wires":[["05197443aab6d823","88932518a5be1a13"]]},{"id":"57a71f4aec71c79d","type":"switch","z":"088145566f68e247","name":"③入力値が番組名か?","property":"payload.genreId","propertyType":"msg","rules":[{"t":"eq","v":"9999","vt":"str"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":140,"y":180,"wires":[["af6dfdd9aaa450a7"],["80eb97197b3ae0f0"]]},{"id":"af6dfdd9aaa450a7","type":"function","z":"088145566f68e247","name":"③-A番組名でない:replyGreeting","func":"msg.payload = '好きな番組名を教えてください。おすすめの番組を紹介します!'\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":420,"y":180,"wires":[["a2bec31e483b2ae4","b334e2bf2d2da7e4"]]},{"id":"782b6edf78f7de01","type":"debug","z":"088145566f68e247","name":"デバッグ4","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":670,"y":260,"wires":[]}]
①ChatGPT APIにリクエスト
node-red-contrib-simple-chatgptパレットのsimple-chatgptノードを利用しています。
Tokenにはご自身で取得したChatGPTのAPIキーを設定してください。
ChatGPT APIの使い方や料金体系については過去記事で解説しているので、お時間ある方はご参照ください。
SystemSettingには以下の値を設定しています。
ユーザが入力した番組を、ChatGPT先生がNHK番組表APIのジャンルに分類します。
以下のジャンルに分類してください。「ID: ジャンル」となっているので、IDの4桁の数値のみ回答お願いします。 0000: ニュース 0100: スポーツ(スポーツニュース) 0205: 情報/ワイドショー(グルメ・料理) 0300: ドラマ(国内ドラマ) 0409: 音楽(童謡・キッズ) 0502: バラエティ(トークバラエティ) 0600: 映画(洋画) 0700: アニメ/特撮(国内アニメ) 0800: ドキュメンタリー/教養(社会・時事) 0903: 劇場/公演(落語・演芸) 1000: 趣味/教育(旅・釣り・アウトドア) 1100: 福祉(高齢者)
②setGenreId
functionノードを利用しています。
NHK番組表APIリクエストの引数であるジャンルIDを取得するためのコードです。
サンプルコード
const userText = msg.payload;
let genreId = '';
// 解説1
// 0000: ニュース
if (userText.indexOf('0000') > -1) {
genreId = '0000';
// 0100: スポーツ(スポーツニュース)
} else if (userText.indexOf('0100') > -1) {
genreId = '0100';
// 0205: 情報 / ワイドショー(グルメ・料理)
} else if (userText.indexOf('0205') > -1) {
genreId = '0205';
// 0300: ドラマ(国内ドラマ)
} else if (userText.indexOf('0300') > -1) {
genreId = '0300';
// 0409: 音楽(童謡・キッズ)
} else if (userText.indexOf('0409') > -1) {
genreId = '0409';
// 0502: バラエティ(トークバラエティ)
} else if (userText.indexOf('0502') > -1) {
genreId = '0502';
// 0600: 映画(洋画)
} else if (userText.indexOf('0600') > -1) {
genreId = '0600';
// 0700: アニメ / 特撮(国内アニメ)
} else if (userText.indexOf('0700') > -1) {
genreId = '0700';
// 0800: ドキュメンタリー / 教養(社会・時事)
} else if (userText.indexOf('0800') > -1) {
genreId = '0800';
// 0903: 劇場 / 公演(落語・演芸)
} else if (userText.indexOf('0903') > -1) {
genreId = '0903';
// 1000: 趣味 / 教育(旅・釣り・アウトドア)
} else if (userText.indexOf('1000') > -1) {
genreId = '1000';
// 1100: 福祉(高齢者)
} else if (userText.indexOf('1100') > -1) {
genreId = '1100';
} else {
genreId = '9999';
}
// 解説2
// 日付をYYYY-MM-DDの書式で返すメソッド
function formatDate(dt) {
dt.setHours(dt.getHours() + 9);
var y = dt.getFullYear();
var m = ('00' + (dt.getMonth() + 1)).slice(-2);
var d = ('00' + dt.getDate()).slice(-2);
return (y + '-' + m + '-' + d);
}
msg.payload = {
"genreId": genreId,
"date": formatDate(new Date())
};
return msg;
// 解説1
こんな感じの機能を作ろう(設計) パート の 2. ChatGPTが入力された番組をジャンル分けするでも紹介した通り、ChatGPTはジャンルIDの数値のみ返答することは難しいようです。
したがって、以下の条件分岐をJavaScriptで実現しました!
- ChatGPTの回答に数値が含まれている=ジャンル分けに成功していたら、該当のジャンルIDを返す
- ChatGPTの回答に数値が含まれていない=ジャンル分けに失敗していたら、9999を返す(ユーザが番組名でない単語を入力した場合などです。ジャンルIDと一致しない値ならなんでもOK)
// 解説2
formatDate関数は、先人が作ってくださった関数を参考に一部改変しています。
日付をYYYY-MM-DDの書式で返す関数 (JavaScript)
Node-REDの環境では、時刻が世界標準時で取得されてしまうようです
そのためこの行を追加し、世界標準時を日本標準時に変更しています
dt.setHours(dt.getHours() + 9);
③入力値が番組名か?
switchノードを利用しています。
ジャンルIDが9999かどうかで条件分岐させます。
③-A番組名でない:replyGreeting
functionノードを利用しています。
ジャンルIDが取得できなかった場合はNHK番組表APIを叩くことができないため、LINE Botがデフォルトのあいさつ文を返すように設定します。
msg.payload = '好きな番組名を教えてください。おすすめの番組を紹介します!'
return msg;
③-B-1番組名である:番組表APIにリクエスト
http requestノードを利用しています。
{{payload.genreId}}と{{payload.date}}には、②setGenreIdで設定した値が入ります。
今回、地域IDは130:東京、サービスIDはg1:NHK総合1で固定しています。
https://api.nhk.or.jp/v2/pg/genre/130/g1/{{payload.genreId}}/{{payload.date}}.json?key=[ご自身で取得したNHK番組表APIキー]
取得できるJSONデータサンプル
{
"list": {
"g1": [
{
"id": "2023042108414",
"event_id": "08414",
"start_time": "2023-04-22T02:46:00+09:00",
"end_time": "2023-04-22T03:16:00+09:00",
"area": {
"id": "130",
"name": "東京"
},
"service": {
"id": "g1",
"name": "NHK総合1",
"logo_s": {
"url": "//www.nhk.or.jp/common/img/media/gtv-100x50.png",
"width": "100",
"height": "50"
},
"logo_m": {
"url": "//www.nhk.or.jp/common/img/media/gtv-200x100.png",
"width": "200",
"height": "100"
},
"logo_l": {
"url": "//www.nhk.or.jp/common/img/media/gtv-200x200.png",
"width": "200",
"height": "200"
}
},
"title": "超多様性トークショー!なれそめ▽元警察官&元消防士!同性カップル",
"subtitle": "なれそめの数だけいろんな人生の楽しみ方がある!今回は「元警察官&元消防士!同性カップル」田村淳、LiLiCo、那須雄登、アンジェリーナ1/3、語り:水瀬いのり",
"content": "なれそめの数だけいろんな人生の楽しみ方がある!今回は「元警察官と元消防士の同性カップル」自分が男性のことが好きなことを認めれなかったKANE(カネ)さん、KOTFE(コッフェ)さんの気持ちをなかなか受け入れられなかった。しかし「彼を失うことの怖さ」に気づき…。★田村淳、LiLiCo、那須雄登(美 少年/ジャニーズJr.)、アンジェリーナ1/3(Gacharic Spin)★語り:水瀬いのり",
"act": "【司会】田村淳,【語り】水瀬いのり,【ゲスト】LiLiCo,アンジェリーナ1/3,那須雄登,KANE,KOTFE",
"genres": [
"0502"
]
}
]
}
}
③-B-2:番組名である:replyProgram
functionノードを利用しています。
// 解説1
var count = Object.keys(msg.payload.list.g1).length;
var i = Math.floor((Math.random() * (count + 1)));
var programTitle = msg.payload.list.g1[i].title;
var programContent = msg.payload.list.g1[i].content;
// 解説2
var start_time = msg.payload.list.g1[i].start_time.split('T');
// 2-1
var start_date = start_time[0].split('-');
// 2-2
var start_hour = start_time[1].split(':');
msg.payload = 'あなたにおすすめの番組は・・・\n「'
+ programTitle + '」\n'
+ programContent + '\n'
+ start_date[0] + '年' + start_date[1] + '月' + start_date[2] + '日'
+ start_hour[0] + '時' + start_hour[1] + '分スタート!';
return msg;
// 解説1
JSONの要素数以下の乱数iを生成し、取得する番組情報をランダムで選択しています。
// 解説2
番組開始時間を○年○月○日○時○分の形に成形するためのコードです。
start_timeは2023-04-22T02:46:00+09:00
の形になっているため、
- 2-1:Tの前後で
2023-04-22
と02:46:00+09:00
に分ける - 2-2:年月日はハイフンで分け、時刻はコロンで分けて時分(02と4)のみ使用する
実装パートは以上です!お疲れさまでした
完成!
新たな作品を開拓する暇のないあなたへ。
— 新米SEちーろってぃ@新婚💍 (@chiiirotty) April 21, 2023
LINE Botがあなた好みのテレビ番組を提案します📺 #protoout #LINEBot #ChatGPT pic.twitter.com/vW7U99V7kz
最初に「こんにちは」と入力すると、デフォルトのあいさつ文が返ってきました!
続いて「ぐるナイ」と入力すると、「きょうの料理」を紹介されました
ごはんつながりかな?
最後に「news zero」と入力すると、「NHKニュース おはよう日本」が紹介されました
無事、大タイパ時代にふさわしいテレビ版のレコメンドツールが完成!
このLINE Botを使えば、自分で探しに行かずとも新たな好きに出会えそうですね
以上!最後までお読みいただきありがとうございました