よろぐ

ブロゴスフィア!ブロゴスフィア!

アジアのインディ音楽を追いかける自動更新ブログ「Asianbreeze」の裏側:Gemini 2.5 Flash × GitHub Actions で作るサーバーレス投稿システム

人間パート

Geminiで、アジアのインディ音楽のニュース記事を自動生成してブログに投稿する仕組みを作ったという話です。 開発物の紹介をAIに色々書いてもらいましたが、まず人間から簡単な補足だけしておきます。

開発物へのリンク

出来上がったはてなブログはこちらです(実際インディロックに興味あればけっこう面白いと思うので見てみてね!) asianbreeze.hatenablog.com

ソースコードはこれです。 https://github.com/ayosino/asianbreeze

技術面の補足

  • GithubActions の実行はcronに設定した時間通りに始まらないことが多く、平気で30分以上遅れるので、8:00に投稿したいなら6:00とかに設定した方がいいです。
  • Difyを使わなかった理由はちょっとAIの説明とは違っており、いろいろちゃんと作り込むならDifyでworkflowを作るなどした方がいい気はしていますが、最小構成としてはpython+GithubActionsでやりたいことができるのと、開発が全てCLIで完結するのでとにかく楽というのが選定理由です。

では残りはAIに任せます。

AI パート

こんにちは!アジアのインディ音楽を愛してやまない開発者です。

韓国のK-Indie、台湾のインディーズ、タイのポップスやインドネシアのサイケデリック・ロックなど、近年アジア各国の音楽シーンが本当に熱いですよね。

今回は、そんなアジアの素晴らしい音楽メディアの最新情報を自動で翻訳・要約し、おすすめのYouTube MVまで添えて毎日勝手に更新してくれるブログ「Asianbreeze」を開発したお話です。

技術スタックは Python + Gemini API (google-genai) + GitHub Actions。 なぜ最近流行りのノーコードLLMツール「Dify」を使わずにこの構成にしたのか、技術的な工夫や挑戦したポイントも含めて紹介します。


1. 開発の動機・モチベーション

アジアのインディ音楽という「沼」、でも情報がバラバラ問題

私自身、日々アジアのインディ音楽をディグるのが趣味なのですが、とにかく情報収集に苦労していました。 * タイの音楽ウェブマガジン FungjaizineDebaser * 台湾のインディーズプラットフォーム StreetVoice (Blow) * 韓国のシーンを伝える Korean Indie * インドネシアのユースカルチャー誌 Whiteboard JournalPop Hari Ini

これらのサイトはどれも非常に良質な音楽を紹介しているのですが、言語がタイ語、中国語、韓国語、インドネシア語と多岐にわたり、毎日手動で翻訳をかけながら巡回するのは骨が折れます。 さらに、これら「国をまたいだアジアのインディ情報を横断して日本語で紹介してくれるサイト」が探してもほとんどありませんでした。

「無いなら、自分が見るためにも、自動で毎朝まとまった紹介記事が上がってくる場所を作ればいいじゃないか」

これが、このプロジェクト「Asianbreeze」の始まりです。


2. 開発したシステムの紹介

概要

アジア各国の主要インディ音楽メディア(現在7つの主要サイトをサポート)から最新の記事を取得し、Gemini APIを利用して日本語の紹介記事を生成し、はてなブログへ直接自動で予約投稿するシステムです。

詳細(パイプラインの流れ)

GitHub Actionsにより、毎日午前6:00(JST)に以下のパイプラインが完全に自動で動作します。

graph TD
    A[GitHub Actions 毎日AM 6:00起動] --> B[各音楽メディアのスクレイピング/RSS取得]
    B --> C[はてなブログAPIから過去記事を取得し、URL重複チェック]
    C -->|重複なし| D[Gemini API による構造化データ生成]
    D --> E[YouTube スクレイピング検索 <br> 最新MV & 人気MVの自動取得]
    E --> F[はてなブログ API AtomPub 経由での予約投稿]
    F --> G[自動で2時間間隔の予約時刻に配置]
  1. スクレイピングとRSS取得: 各対象サイト(StreetVoice, What The Duck, Debaserなど)の最新エントリーやRSSフィードを取得。
  2. 重複投稿チェック: はてなブログのAPI(AtomPub)から過去の記事(直近数ページ)を取得し、既に投稿済みの紹介元URLであれば処理をスキップ。
  3. Gemini APIによる記事生成: 最新の google-genai SDK を使い、モデルには gemini-2.5-flash を使用して、元記事からアーティスト名、ジャンル、経歴、元記事の趣旨(150字程度)、日本の読者向けにローカライズされたタイトルを生成。
  4. YouTube MVの自動選定: アーティスト名と曲名をキーにYouTubeを検索し、最新のMVと最も再生回数が多いMVの2種類を自動でピックアップ。
  5. はてなブログへの自動予約投稿: Markdown形式の記事を組み立ててAtomPub APIで送信。朝8:00から2時間おきに自動で時間がズレるようにマッピングし、1日の間に定期的に新しい記事が公開されるようにします。

3. 技術的に挑戦的だった部分とこだわり

① Gemini APIの「構造化出力(Structured Outputs)」による堅牢化

LLMを使った自動化で最も厄介なのが「出力のブレ」です。JSONで返してほしいのに余計なテキストが混ざったり、キーの名前が変わったりするとプログラムがクラッシュします。 今回は、新しい google-genai SDKの Structured Outputs 機能を利用し、Pydanticのスキーマを直接定義してGeminiに渡しています。

class ArtistIntro(BaseModel):
    name: str = Field(description="アーティスト名")
    description: str = Field(description="アーティストの紹介(1行)")

class HatenaBlogParts(BaseModel):
    is_compilation: bool = Field(description="複数アーティストを紹介する記事か否か")
    blog_title: str = Field(description="はてなブログ記事のキャッチーなタイトル")
    genre: List[str] = Field(description="アーティストのジャンル(限定されたリストから選択)")
    bio_style: str = Field(description="経歴・作風")
    youtube_query: str = Field(description="YouTube検索クエリ")
    article_purpose: str = Field(description="元記事の趣旨の要約")
    compilation_artists: List[ArtistIntro] = Field(description="複数紹介時のリスト")

このように定義することで、Geminiから確実にこのスキーマ通りのJSONが返ってきます。 また、プロンプトで「単一のアーティスト紹介記事か、それとも新曲まとめなどのオムニバス(コンピレーション)記事か」を is_compilation フラグで判定させています。これによって、ブログのテンプレート(見出しや構成)を動的に切り替えることができるようになりました。

② YouTube Data APIの「クォータ制限」を回避するスクレイピング

通常、YouTubeをプログラムから検索するには「YouTube Data API」を使いますが、このAPIには1日のクォータ(利用上限)が厳しく設定されており、毎日の自動更新で何件も検索をかけるとすぐに上限に達してしまいます。 そこで今回は、YouTubeの検索結果ページに埋め込まれている ytInitialData というJSONデータを正規表現で直接パースするロジックを実装しました。

APIキー不要で、最新動画と最も再生回数(Popular)が多い動画を的確に抽出し、はてなブログの埋め込み用タグ( [https://www.youtube.com/watch?v=xxx:embed] )を動的に生成しています。これにより、API制限を一切気にせず、完全無料で安定してYouTubeのMV動画を差し込めるようになりました。

③ 8:00から2時間おき!スマートな予約投稿(スケジュール公開)マッピング

毎日同じ時間(例えば朝6時)に7サイト分の記事が一気に投稿されると、読者にとってもスマートではありません。 そこで、GitHub Actionsが早朝6:00に動作した際、自動投稿オプション(--scheduled)をONにすると、自動で「1記事目は朝8:00、2記事目は10:00、3記事目は12:00……」と投稿予定時間を算出し、はてなブログ側で「予約投稿(指定日時公開)」状態にしてAPI送信するロジックを実装しました。 これにより、何もしなくてもブログが日中定期的に更新されているかのような自然な見え方を実現しています。


4. 技術選定の裏側:なぜDifyではなく「Python + GitHub Actions」だったのか?

LLMを使った自動化・ワークフロー構築といえば、今や Dify が非常に人気です。ドラッグ&ドロップでエージェントやLLM処理を繋ぐことができ、最初のステップとして当然私もDifyの使用を検討しました。

しかし、比較検討した結果、最終的には自作の Pythonスクリプト + GitHub Actions を選択しました。その主な理由は以下の5点です。

比較項目 Dify (Cloud / セルフホスト) Python + GitHub Actions (今回採用)
運用コスト サーバー代(VPS等)またはCloudの課金枠 完全無料(GitHub無料枠+Geminiの個人枠)
スクレイピングの柔軟性 サイト構造が変わると対応が難しい BeautifulSoupで柔軟にコード対応可能
重複・ステート管理 データベースやカスタムコードが必要で煩雑 はてなブログの過去記事APIとメモリ上で即座に照合可能
APIクォータ制限 YouTube APIのクォータを消費せねばならず詰まる スクレイピングによるハックで制限なしで実行可能
バージョン・履歴管理 UIベースのインポート/エクスポートが必要 すべてGitリポジトリ(YAML, .py)で完結

Difyを見送った具体的な経緯:

  1. コストをゼロに抑えたい: 個人用の趣味プロジェクトなので、常時稼働させるサーバー費用は1円も払いたくありませんでした。GitHub Actionsならパブリックリポジトリならビルド時間は無料。Gemini APIの実行費用も、個人で回す分にはほぼ無視できるレベル(あるいは無料枠)です。
  2. スクレイピングの自由度: 相手は海外のインディーズメディアなので、HTMLのDOM構造が独特だったり、JavaScriptで一部読み込まれるコンテンツがあったりします。これらを柔軟にパースするには、Difyの標準ノードよりPythonの BeautifulSoup や正規表現を使うほうが結局手軽でした。
  3. はてなブログの過去記事との重複チェック: Difyのワークフロー上で「はてなブログの過去記事のタイトルやURLをAPIから全件取得して、今回のスクレイピングデータの中に重複があるかをループで精査する」という処理は、できないことはないですがワークフローが非常に複雑になります。Pythonなら数行のリスト内包表記で書けるため、コードのほうが圧倒的に見通しが良かったです。

結果として、Difyのようなノーコードツールは「一般的な定型業務」には向いているものの、今回のような「ちょっとした職人芸的なハック(YouTubeの内部JSONパースなど)」や「サイト毎のスクレイピング分岐」が要求される開発では、コードでガシガシ書いてGitHub Actionsに載せる構成が最も開発・保守しやすかったです。


5. まとめ

自分自身の「こんな音楽ブログが毎朝読めたらいいのに」という熱量からスタートした開発ですが、生成AI(Gemini API)の翻訳・要約力と、GitHub Actionsのタイマー起動を組み合わせることで、「自走するアジアインディ音楽ブログ」がものの数日で誕生しました。

言語の壁を軽々と超えて、毎日タイや台湾、韓国、インドネシアのローカルで熱い音楽が日本語の紹介テキストとYouTube動画付きで目の前に届くのは、音楽ディグ用として控えめに言っても最高です。

同じように「自分だけの自動更新メディアやキュレーションサイトを作ってみたい!」という方の参考になれば幸いです。

ぜひ、アジアのインディ音楽の世界(Asianbreeze)をチェックしてみてください!