昨今の生成AIとワークフローオートメーションを使って、もう10年以上ガチ安定で稼働している自宅のEPGStationの録画サーバーに「AIがオススメする録画候補番組リスト」という新機軸を追加する試み。構想30分、構築一週間くらいの作業記録です。
今回はひとまず全て無償の枠内で動かすことを前提に。APIの無償枠が安定して使えるのはおそらくGeminiだけなので、Geminiで組みました。
成果物
Geminiがオススメ番組を選んでリストアップしてくれるWebページ。

- 過去の録画実績と、個別にプロンプトで指示した条件を元に、Geminiが私と家族の視聴傾向を判断して放送リストの中からオススメ番組を拾ってくる
- リストには予約ボタンがついていて、直接EPGStationに予約登録ができる。予約済みの番組はグレーアウトする
- リストは一日に二度、朝と晩に更新される
- 長すぎてもアレなので、現状では提案件数は最大30件までに制限
サンプルのn8nワークフロー
そのままでは動かないだろうが参考までに。n8nにインポートすれば手軽に雰囲気は分かると思う。各ノードの設定やJavaScriptについてはこちらを参照していただきたい。実際に動かすにはData Tableの作成や環境変数の設定、Gemini API連携の設定などの作業が必要。環境固有の情報はダミーに入れ替えてある。
自宅のLAN内でのみ使う前提なので、CORSを全開で許可していたりなどセキュリティ的には甘い作りになっている点には注意していただきたい。またコードは全てGeminiに書いてもらっているのでコメントや注釈は一貫性に欠けているかもしれない
使ってみた感想
- リストに上がってくる番組は「惜しい」ものが多く、実際に予約に至るのは一回の提案に一番組あるかないかで打率は低い。しかし自分で手検索するよりは効率はずっと高く、母集団のフィルタリングとしては十分に効果的。プロンプトの改良や学習方法の変更で良くなる可能性が高いので、またトライしたい
- 推奨理由が面白い。たまに、自力では到達しないであろう番組を引っ張ってくるのも味わい深い
- 毎日推薦された番組リストを見てるだけでも結構楽しめるので、良しとする
では、ここからは構築について。
前提となる環境
- EPGStationでの録画システムが既に稼働中であること
- そのホストであるLinuxサーバーが稼働していること(WindowsでもOK)
- Dockerが動かせること
筆者のスペック
- Linuxの一通りの操作ができてEPGStationを動かせる程度のスキル
- コードは全く書けない。簡単なシェルスクリプトがギリ
- n8n、というかワークフローオートメーション系を触るのはこれが初めて
やりたいこと
- 自分が興味を持ちそうな番組の候補をAIに探させて提案してもらう
- 提案リスト内から直接録画予約ができる(EPGStationに戻らなくていい)
- 定期的に勝手に動いて提案してくる
作業の進め方
上記の構想と成果物のイメージをGeminiに示して、EPGStationとn8nを使いたいと伝えたら、あとはひたすらGeminiと会話して指示に従って組んでいく。マンツーマンの講師がついたワークショップみたいな感じ。n8nをどう使い始めるのか?から教えてくれるし、JavaScriptも秒で書いてくれる。はっきり言って信じられない。なんつー世の中だ。。ただしこの講師は当然ながら色々と間違えるので、付き合い方を学ぶ必要がある。初めて本格的にAI対話型で組んでみた感想はこのエントリの後半に。
実際の構築手順
- Ubuntu上にDockerでn8nを導入する
導入手順はGeminiが教えてくれる - Dockerのdocker-compose.ymlにn8n用の環境変数とアクセス許可を設定する。太字の部分
==================
version: '3.8'
services:
n8n:
image: n8nio/n8n:latest
container_name: n8n
restart: unless-stopped
ports:
- "5678:5678"
volumes:
- ~/n8n_data:/home/node/.n8n
- ~/n8n_data/tmp:/tmp
environment:
- N8N_SECURE_COOKIE=false
- GENERIC_TIMEZONE=Asia/Tokyo
- TZ=Asia/Tokyo
- N8N_RESTRICT_FILE_ACCESS=false
- N8N_RESTRICT_FILE_ACCESS_TO=/tmp
- EPGSTATION_URL=http://[EPGStationのホスト名またはIPアドレス]:8888
- N8N_WEBHOOK_BASE_URL=http://[n8nのWebhook用ホスト名またはIPアドレス]:5678
- N8N_BLOCK_ENV_ACCESS_IN_NODE=false - Google AI StudioでGemini APIを有効にする
この手順もGeminiが教えてくれる - n8n上にData Tableを作成する
ai_cacheという名前で作る。keyはlatestのみ。こんな感じになる

- n8n上にノードを配置していく
ワークフロー構成
全体像はこんな感じ。

処理のざっくりした流れはこう。
- EPGStationから、向こう2日間の番組表、予約済み番組リスト、録画済み番組リスト、の3種類のデータをAPI経由で取得する
- 取得したデータを合流させて整形し、余計な情報を削ってデータ量を絞る
- 絞ったデータにプロンプトを添えてGeminiに食わせる
- Geminiから出てきたオススメ番組リストをn8nのData Storeに保管する
- リストが更新された旨をメールで利用者に通知する
- 利用者がWebブラウザでオススメ番組リストにアクセスする
- n8nがData Storeから番組データを読んでHTMLを生成してブラウザに返す
- 録画したい番組の予約ボタンを押すとEPGStationにPOSTが飛んで予約が実行される
各フェーズとノードごとにもう少々細かい説明をする。
1. データ収集・前処理フェーズ(定期実行)
まず最初にEPGStationからAIの判断材料となる生データを収集する。
- Schedule Trigger
n8nのワークフローは、起動タイミングを規定するTriggerタイプのノードから開始するルールになっている。今回は毎日 07:15 と 18:15 の2回、ワークフローを自動で走らせることとした。一日の間に頻繁にオススメリストを更新してもあまり自分には意味がないので、朝晩の2回更新で良しとする。 - HTTP Request (schedules)
予約候補となる番組リストの母集団をEPGStationからSwagger APIで取得する。EPGStationは向こう一週間の全チャネルの番組表を持っているが、これを全部取得すると相当なデータ量になってしまい、後ほど後段でGeminiに投げる情報量が多過ぎてトークン上限をオーバーするので、この段階である程度範囲を絞る必要がある。まず、私はCSはほぼ見ないので、取得対象は地デジとBSのみとした。さらに対象レンジはタスク実行時から1日先~3日先の2日間とした。さらに放送局を絞って、代わりに期間を延ばすなどのアレンジをここで行うことも可能だが、それは後段で実施することとして、一旦これで行く。 - HTTP Request (recorded)
EPGStationですでに録画済みの番組リストを取得する。すでに録画された番組は利用者の視聴傾向を反映しているはずなので、このリストを後ほどGeminiがユーザーの好みを分析するための材料として利用する。 - HTTP Request (reserves)
EPGStationで現在すでに予約登録されている番組リストを取得する。すでに予約済みである番組がGeminiから重複推薦されないように、除外リストとして使用する。 - Merge
上記3つのAPIから取得した異なるデータを一つの流れに統合し、以降の処理を一本にまとめられるように整形する。 - Trim (Codeノード)
取得した生データから必要な項目(タイトル、概要、IDなど)のみを抽出し、既に予約済みの番組を候補から除外するなどの整理を行う。 - Aggregate
分散しているデータアイテムを一つにまとめ、次の「絞り込み」工程に渡せる形式に整形する。n8nはアイテムの単位で処理を実行するので、ここでまとめておかないと後段でGeminiにバラバラのリクエストが複数回に分かれて投げられることになり、期待する回答が得られなくなる。
※考えてみると前段のTrim内でも同じ処理を実行できるので、このノードは削れるかもしれない
2. AI向け最適化・推論フェーズ
AIが効率よく正確に判断できるようにデータを加工し、さらにデータ量を絞り、その上でプロンプトとして投入する。本システムのキモとなる部分。
-
Slim for Gemini (Codeノード)
前段で放送種別や時間帯を絞って番組表を取得したが、それでもなお生の番組リストは情報量が多く、絶対に録画対象にならない番組や、AIの処理には不要なデータが大量に含まれているので、ここでそれらを除外する。放送局名、キーワードリスト、放送時間(10分以下)での除外を併用してフィルタリングする。関東地方での地デジとBSでは2日間で概ね2000件程度の番組が放送されるが、このノードでこれを約700件程度まで絞り込む。さらにAI処理およびその後段で必要になる情報(タイトル、概要、ID、開始時刻)以外をカットし、Geminiに渡すデータ量を減らす。 - Prompt (Codeノード)
Geminiに対してのプロンプトを書くノード。プロンプトは後段のGeminiノード内に書いても構わないのだが、流れで作業していたらここで書く作りになった。プロンプトに含めるべき要素は大きく3つある。
-
まずは「選定の基準と指針」。利用者の関心があるもの、逆に興味がないもの、優先度を上げて/下げてほしいもの、などの指示をキーワードや文章でAIに伝える
- 次に「出力形式」。後段で正しく処理ができるように、データの出力形式をシンプルかつ明確に指定する。ちゃんと定義しておかないと出力の揺れが発生して処理がエラーになったり、推奨データがおかしなことになったりする。特に各番組の一意の識別子となるProgramIDは確実にキープするよう指示する
- 最後に「抽出用の元データ」。前段で整形した番組表と、嗜好判断用の録画済み番組リストを渡す
プロンプト自体はこのエントリのもう少し先に記載した。
-
3. 保存・通知フェーズ
Geminiの出力した推奨リストをローカルストレージに保存し、ユーザーに更新完了を通知する。
-
Upsert row(s)
Geminiが選んだ推薦番組リストと、詳細情報を含むマスターリストを、n8n内部のデータテーブル(ai_cache)に保存する。データテーブルは直近のGeminiの出力結果だけを保持する超シンプルな作り。ワークフローが走るたびに毎回更新され、最新の結果で常に上書きされる。過去の情報は持たない -
Send email
ダッシュボードの更新をメールで通知する。ユーザーは自分と家族しかいないし、日に二度実行されることは分かっているので必須ではないし、処理が失敗したときにだけエラー通知すれば用は足りるのだが、しかし正常に稼働してますよというプッシュ型通知の意味と、あと通知が届くとなんとなく嬉しいので賑やかしとして付けた。
4. ユーザーインターフェース(ダッシュボード)表示フェーズ
ユーザーがブラウザで推薦結果を確認して予約操作を行うためのWebページを生成するプロセス。独立したサブワークフローとして設置する。このフローをメインのフローに結合してしまうと、ページを開くたびにGeminiが走って処理が非常に重くなる上に、早々にトークン超過で利用不可になってしまうので、ダッシュボードを表示するだけの別プロセスとして切り離す。
-
Webhook-dashboard
Webサーバーの口としてHTTPリクエストを待ち受ける。リクエストを受けて以降のワークフローをキックする - Get row(s)
データテーブルから最新のAI推薦データを読み込む -
HTTP Request (reserves)2
ダッシュボードを表示する瞬間の最新の予約状況をEPGStationから取得する - Prepare Webhook URL
環境変数からWebhook用のURLを取得して後段のCodeノードにwebhook_urlとして渡す。n8nではCodeノードでJavaScriptから直接環境変数を読めない(とGeminiが言っていた)ので、このノードを追加する流れになった -
Dashboard (Codeノード)
読み込んだデータをHTML/JavaScriptに変換し、番組名、推奨理由、「予約ボタン」が並ぶWeb画面のデザインを生成する -
Respond to Webhook
ブラウザに対して、完成したHTMLダッシュボード画面を返送する
5. 予約実行フェーズ
ダッシュボード上の予約ボタン押下を受けて実際にEPGStationに録画予約を行うプロセス。
-
Webhook-reservation
ダッシュボードの「予約する」ボタンが押された際に発行されるPOSTリクエストを待ち受ける。POSTリクエストには番組IDが含まれる -
POST to EPGStation
受け取った番組IDをAPI経由でEPGStationにPOSTリクエストとして送信し、録画予約を確定させる
Geminiに渡すプロンプト例
# ROLE
あなたは、私が視聴するべき番組を選定し推奨する専任コンシェルジュです。# HOW TO THINK
- 【REFERENCE】から私の視聴傾向・好みを推定してください
- 【CANDIDATES】の中からのみ、推薦番組を選んでください
- 【REFERENCE】に含まれる番組自体を推薦してはいけません# USER PREFERENCE (EXPLICIT)
・関心があるもの:
旅行、日本の地方文化、美術、芸術、音楽、自然、動物、鉄道、
ドキュメンタリー、料理、食事、日本の伝統、離島、宇宙・関心がないもの:
バラエティ、スポーツ中継、通販、ショッピング、釣り、自動車・注目キーワード:
北海道、富山、瀬戸内、山陰、孤独のグルメ、tiny desk concert# SELECTION RULES
- 再放送(タイトルに [再] を含む番組)は優先度を下げる
- スポーツの試合中継番組は優先度を下げる
- 選定する番組のカテゴリーが極端に偏らないようにする(条件に該当する番組が少ない場合は偏っても可)
- 最大で30件まで選定(条件に該当する番組が少なければ30件未満でも可)
- あなたは、私の視聴傾向に必ずしも合致しない可能性がある番組を、最大で3件まで、あなたの判断で選んでも構いません# OUTPUT
- 選定した番組の ID [programId] と 番組名 [title] と 推薦理由 [reason] のみを、以下のフォーマットのJSON形式で出力してください
[{"ID": "...", "title": "...", "reason": "..."}]
- IDに格納するprogramIdの値は元のデータから一切変更せず、そのまま用いてください# REFERENCE: WATCH HISTORY(嗜好推定用・参考情報)
${recordedText}# CANDIDATES: PROGRAM GUIDE(推薦候補・ここからのみ選択)
${schedulesText}
開発アシスタントとしてのGeminiの使い勝手
- FlashとThinkingを意図的に使い分けると効果的。全体デザインはThinkingで、個別のノード設定やシンプルなJavaScriptの作成はFlashで。Flashで何回試してもコードが修正できない場合はThinkingに切り替える。n8nはワークフローをJSONファイルとしてダウンロードできるので、行き詰ったらJSONをGeminiに食わせてThinkingモデルで全体レビューと解決方法の提案をしてもらうと突破しやすい
- 特にFlashモデルはフロー全体のデザインや整合性を俯瞰で見るのが苦手なので、今の作業は本当に期待するゴールに向かっているか?は自分で配慮しながら進める必要がある。任せきりにすると、例えば推奨リストを見るたびに毎回Gemini APIを叩く(=アクセスの度に長時間待たされる上にトークンがすぐに上限を超える)デザインを提案してきたり、データの保管先に生のJSONファイルを使おうとしたり、何もかもGeminiへのプロンプトに投げ込もうとしたりする。「これはおかしくね?」とか「違う手段のほうがいいのでは」と思ったら迷わず指摘するのが重要
- Geminiは比較的粘りと頑張りに欠けていて、ある意味現実的なところがあり、何度か試して上手くいかない課題があると、正面突破を諦めて別の回避策を提案してくる傾向がある。提案は妥当なこともあるが、雑で安易な逃げの手だったりもするので、採用するかどうかの判断はしっかり行う。ちなみにChatGPTは愚直に課題に挑むが解決できずにループに陥ったりする。キャラクターの違いが中々味わい深い。クールで冷静なClaudeが一番こういうのには向いている気がする
- n8nの使い方に関しても詳しく教えてくれるが、情報が古かったりして、実際にメニュー内に存在しないオプションを設定するよう指示することが頻繁にある。その都度、実際のメニューはこうなっているが、どうすればいいか?と再質問するか、自分で適当に設定した上でJSONを食わせてチェックを依頼するといい
- スクリプトはすぐに書いてくれるが、動かないことも多い。一発で成功すると思わず、エラーの情報を与えたり前段からのデータを添えるなどして、周辺情報を与えつつ何度か修正を繰り返して徐々に先に進んでいく。トライアンドエラーとブラッシュアップを粘り強く繰り返す。粘り強く、と言っても自分一人でやるより100倍速いので文句は全くない。しかも無償だし。あなありがたや
- Geminiが書いたコードをChatGPTなどの別のAIに評価させるのもアリ。Geminiが書いたコードをChatGPTがコキ下ろすシーンに何度か遭遇し、結構笑えた
今後やりたい追加実装
- 録画済み番組だけではなく、予約済み番組も傾向把握材料としてGeminiに食わせてもいいかもしれない。あんまり変わらなさそうだけど
- Aggregateノード周辺の処理は少々冗長な気がするので見直したい
- 傾向把握に使う録画履歴は「その時点でEPGStation上に残っている録画の最新100件」のみを都度使う作りになっている。つまり長期間の視聴傾向の蓄積はしていないし、使い込むほどGeminiのアルゴリズムが洗練されたりはしない、なので、時間経過とともに育つコンシェルジュに挑戦したい。今のGemini APIでは過去の会話の履歴を長く引き継げないはずなので、ちょっとハードルが高そうだが...
苦労したところ
- 番組表のデータ量なんて大したことないでしょと思っていたがそんなことはなかった。当初は一週間分まるっとEPGStationから拾って全部Geminiに食わせりゃOKなどと安易に考えていたが、秒でトークン超過することが分かり、データの品質をキープしつつ量を削る手法を考えるのに結構な時間を使うことになった
- 初期段階でGeminiの指示に安易に従った結果、推奨番組リストも含めたHTMLダッシュボード出力までの全てをGeminiに任せるフローが組み上がった。これは上手く動くこともあるが結構な確率で失敗する(Geminiからの出力がブレてHTML構造が壊れる)上にトークン量が増加するという欠点があり、路線転換するのが結構面倒であった
- Geminiからの出力データを毎回同じ形式になるよう厳密に守らせるのが意外と大変であることが分かった。例えば、この構築の初期段階では、Geminiに推奨候補を選ばせるだけでなく、それをリスト化してHTMLページを生成するところまでを全部やらせようとしたのだが、実行のたびにタグ構造が壊れたとかJSONじゃないとかスクリプトエラーだとかバリエーション豊かなパターンでフローが失敗して使い物にならなかった。これを防ぐために、Geminiからの出力は最小限のデータ量で極力シンプルな形になるよう変更し、HTMLの生成や予約済みデータとの照合は別途切り出してJavaScriptでの実装とした。適材適所ということである
- 同様にGeminiが長いリンクの文字列を何故か一部だけ変更してしまいリンク切れになる問題が多発したので、明示的に変更しないよう指示して概ね抑制できるようになった(それでも極稀におきるが)
その他雑感など
- 月並みな感想だが生成AIヤバい。Web開発経験はなく、コードも書けず、ワークフローツールに触ったこともない程度の人間がちょっと作業しただけでこのくらいの仕組みを組めてしまうとは。なんつー時代だ
- 毎度のことですがDocker様本当にありがとうございます
- EPGStationの作者様に改めて感謝。今回ようやくAPIをフル活用できました
- 一部のベンダーがノーコードノーコード騒ぐ理由を実感できた
- 今回は推奨番組ダッシュボードという形式にしたが、ちょっとアレンジすればn8nが勝手にEPGStationに録画登録する形にすることも可能。しかし今のところは打率が低いので、最後は人の手で選ぶのがちょうど良さそうである
- 少し時間が経過したら、このようなフローを組まずとも、OpenClawとかでもっと簡単な指示で勝手にニーズを満たしてくれるようになりそうな気がする
では皆さま、よい録画生活を。
















