見出し画像

生成AIの最新ニュースをslackにフィードするシステムを作った話【レシピも公開】

この記事は 株式会社ベーシック Advent Calendar 2024 、12日目の記事です。


こんにちは。株式会社ベーシックでデータアナリストをしています、AKBと申します。ベーシックの2024年のアドベントカレンダーということで私からは生成AIの最新ニュースを自動でslackにフィードするシステムを作りましたのでそのレシピを紹介したいと思います。

レシピ

細かい背景等を説明する前に先に一番皆さんが知りたいであろうレシピを公開したいと思います。

全体構成

まず、YouTubeで生成AIの情報を配信しているチャンネルを探し、そのチャンネルの動画概要の文章などの最新動画情報をRSS*から取得します。YouTubeチャンネルに新しい動画が投稿されたらその情報が入ってくるイメージです。次に取得した文章をGeminiでSlack投稿用に加工してもらいます。最後にSlackのwebhookを使ってGeminiで加工した文章を対象のチャンネルに配信します。

*RSSとはRich Site Summaryの略でYouTubeなどのウェブサイトの情報を保持しているものです。

事前準備

まずは以下の4つを用意してください。

1. YouTube動画の情報を保持するスプレッドシートのスプレッドシートIDとシート名
2. YouTube動画を取得したいチャンネルのチャンネルID
3. Gemini APIを使うためのAPI key
4. Slackに投稿するためのwebhook URL

1のスプレッドシートIDはスプレッドシートのURLのd/の後から/editの前までの文字列を指します。シート名はデフォルトで「シート1」となっているものです。

https://docs.google.com/spreadsheets/d/{スプレッドシートID}/edit?gid=0#gid=0

2のチャンネルIDですが取得したい動画のチャンネルページへ行ってチャンネル欄の「さらに表示」を押して「チャンネル共有」を押すと取得することができます。

「さらに表示」をクリック
「チャンネルを共有」をクリック

3のGeminiのAPI keyですがこちらのページから取得できますので、漏洩に注意しながら保管してください。

4のwebhook URLはこちらのサイトの流れに従って設定すればURLが発行されます。


設定手順

まずスプレッドシート開いて「拡張機能」の「Apps Script」をクリックします。

「拡張機能」> 「Apps Script」

エディタが開くと思いますので以下のコードをコピー&ペーストしてください。真ん中のconst promptの部分は取得したYouTube動画の情報をどのように加工するかを指示しているプロンプトです。ここは使う方の希望に合わせて変更してください。

function main() {
  const channelId = getScriptProperty("CHANNEL_ID");
  const videoData = fetchYouTubeChannelRSS(channelId);
  if (videoData) {
    const newVideos = filterNewVideos(videoData);
    if (newVideos.length > 0) {
      const videosWithNewsletters = addNewslettersToVideos(newVideos);
      writeVideosToSpreadsheet(videosWithNewsletters);
      videosWithNewsletters.forEach(video => {
        sendSlackNotification(video);
      });
    }
  }
}

// YouTubeのRSSを取得する関数
function fetchYouTubeChannelRSS(channelId) {
  const rssUrl = `https://www.youtube.com/feeds/videos.xml?channel_id=${channelId}`;

  try {
    const response = UrlFetchApp.fetch(rssUrl);
    const xml = response.getContentText();
    const document = XmlService.parse(xml);
    const root = document.getRootElement();

    const ns = XmlService.getNamespace("http://www.w3.org/2005/Atom");
    const ytNs = XmlService.getNamespace("yt", "http://www.youtube.com/xml/schemas/2015");
    const mediaNs = XmlService.getNamespace("media", "http://search.yahoo.com/mrss/");

    const entries = root.getChildren("entry", ns);
    const videoData = entries.map(entry => {
      const title = entry.getChild("title", ns).getText();
      const videoUrl = entry.getChild("link", ns).getAttribute("href").getValue();
      const published = entry.getChild("published", ns).getText();
      const videoId = entry.getChild("videoId", ytNs).getText();

      const mediaGroup = entry.getChild("group", mediaNs);
      const description = mediaGroup ? mediaGroup.getChild("description", mediaNs).getText() : "説明なし";

      return { title, videoUrl, published, videoId, description };
    });

    return videoData;
  } catch (error) {
    console.error("エラーが発生しました:", error);
    return null;
  }
}

// 新しい動画のみをフィルタリングする関数
function filterNewVideos(videoData) {
  const spreadsheetId = getScriptProperty("SPREADSHEET_ID");
  const sheetName = getScriptProperty("SHEET_NAME");
  const sheet = SpreadsheetApp.openById(spreadsheetId).getSheetByName(sheetName);

  const lastRow = sheet.getLastRow();
  if (lastRow < 2) {
    return videoData; // シートにデータがない場合、すべての動画が新しいもの
  }

  const videoIdRange = sheet.getRange(2, 7, lastRow - 1, 1); // 2行目からデータのある最後の行までのG列
  const lastVideoIds = videoIdRange.getValues()
    .flat()
    .filter(id => id)
    .map(id => id.toString().trim());

  return videoData.filter(video => !lastVideoIds.includes(video.videoId.toString().trim()));
}


function addNewslettersToVideos(videoData) {
  return videoData.map(video => {
    const newsletter = createNewsletterWithGeminiAPI(video.videoUrl, video.description);
    return { ...video, newsletter };
  });
}

// GeminiでYouTubeの動画情報をSlack投稿用の文章に変換する関数
function createNewsletterWithGeminiAPI(videoUrl, description) {

  const prompt = `#指示
  「動画URL」の情報とYouTube動画の「説明」の情報から「#出力例」のようなニュースのヘッドラインを作成してください。

# 動画URL: ${videoUrl}
# 説明: ${description}

# 出力例
【今週のAIニュース】
1. {説明から抽出したニュースヘッドライン1}
2. {説明から抽出したニュースヘッドライン2}
3. {説明から抽出したニュースヘッドライン3}
...
`; //プロンプトは各自で変更可能

  const geminiApiKey = getScriptProperty("API_KEY");
  const geminiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${geminiApiKey}`;

  const payload = {
    contents:[
        {
          role: "user",
          parts: [{'text': prompt}]
        }
      ]
  };

  const options = {
    method: "post",
    headers: {
      "Content-Type": "application/json"
    },
    payload: JSON.stringify(payload)
  };

  try {
    const response = UrlFetchApp.fetch(geminiUrl, options);
    const result = JSON.parse(response.getContentText());
    console.log(result.candidates[0].content.parts[0].text);
    return result.candidates[0].content.parts[0].text;
  } catch (error) {
    console.error("Gemini API呼び出し中にエラーが発生しました:", error);
    return null;
  }
}


// 動画の情報をスプレッドシートに投稿する関数
function writeVideosToSpreadsheet(videos) {
  const spreadsheetId = getScriptProperty("SPREADSHEET_ID");
  const sheetName = getScriptProperty("SHEET_NAME");

  const spreadsheet = SpreadsheetApp.openById(spreadsheetId);
  const sheet = spreadsheet.getSheetByName(sheetName);

  if (sheet.getLastRow() === 0) {
    const headers = ["Title", "Video URL", "Published Date", "Video ID", "Description", "Newsletter", "Video ID Log"];
    sheet.appendRow(headers);
  }

  const rows = videos.map(video => [video.title, video.videoUrl, video.published, video.videoId, video.description, video.newsletter, video.videoId]);
  sheet.getRange(sheet.getLastRow() + 1, 1, rows.length, rows[0].length).setValues(rows);

  console.log("スプレッドシートに新しい動画データが追加されました");
}

// slackに情報を投稿する関数
function sendSlackNotification(video) {
  const webhookUrl = getScriptProperty("SLACK_WEBHOOK_URL");
  const message = `*AIニュースの時間です*\n\n${video.newsletter}`;

  const payload = JSON.stringify({ text: message });
  UrlFetchApp.fetch(webhookUrl, {
    method: "post",
    contentType: "application/json",
    payload: payload
  });

  console.log("Slack通知が送信されました");
}

// スクリプトプロパティから情報を取得する関数
function getScriptProperty(key) {
  const scriptProperties = PropertiesService.getScriptProperties();
  return scriptProperties.getProperty(key);
}
コード貼り付け後

次に事前準備のところで用意した情報を入力します。左サイドバーの歯車マークをクリックして「スクリプトプロパティ」のところを開いてください。「スクリプトプロパティを追加」ボタンから入力欄を追加し、「プロパティ」と「値」の部分を以下のように入力してください。

プロパティ:SPREADSHEET_ID 値:事前準備で取得したスプレッドシートID
プロパティ:SHEET_NAME 値:事前準備で取得したシート名
プロパティ:CHANNEL_ID 値:事前準備で取得したチャンネルID
プロパティ:API_KEY 値:事前準備で取得したAPI key
プロパティ:SLACK_WEBHOOK_URL 値:事前準備で取得したwebhook URL

「値」の部分は事前準備で取得したものを記載してください

続いて画面右上の「デプロイ」ボタンから「新しいデプロイ」を選択し、種類の選択で「ウェブアプリ」を選んでからデプロイしてください。

「デプロイ」>「新しいデプロイ」
「ウェブアプリ」を選択して右下のデプロイボタンを押す

最後に左サイドバーのトリガーを選択し「トリガーの追加」を押した後で「イベントソースを選択」を「時間主導型」にすると何時間おきにスクリプトを起動するかを選べるので使用者のお好みで設定します。私はこれは「1時間おき」に設定しています。

「トリガー」>「トリガーの追加」
「イベントソースを選択」を「時間主導型」にして実行間隔を選択

以上の設定をすると指定のYouTubeチャンネルで新しい動画が投稿された時にSlackに情報が投稿されるようになります。お疲れ様でした!!

このシステムを作った背景

さて、レシピも公開してメインディッシュが終わったところですが、こちらのシステムを作った背景だけ共有しようかと思います。

弊社では全社で積極的に生成AIを活用していくという方針を決定をしており以下のような取り組みをこれまで行ってきました。

  1. ChatGPTをTeamプランで全社員分アカウント支給

  2. 生成AI活用講座の開講

  3. 生成AI活用アワードを開催し社内のAI活用を促進

  4. 社内でのAI活用事例を社内報で共有

上記の取り組みについては弊社の以下の記事で解説しております。

また生成AIを活用した成果も徐々に現れつつあります。

しかし、AIの技術進歩の速度は凄まじいため最新情報のキャッチアップの度合いは社員によってムラがある状態でした。このムラを少しでも改善しようという動きが社内で起こり、簡単ではありますが最新ニュースをSlackにフィードするシステムを作りました。

2025年に向けて

私は趣味で生成AIの動向を追っているのですが2024年も凄まじい進歩がありました。特にClaudeのArtifacts機能は革命的だと考えていて、その後からアプリの完成画面をプレビューしながらチャットするという体験が今でも流行り続けています。ちょうど1年前の今頃はGPTsが登場して非常に興奮したのを覚えていますが、今では資料を読み込ませて文章を生成するのは割と当たり前の機能になってきている気もします。そう考えると来年の今頃には完成品をプレビューしながらチャットするという体験は当たり前になっているのかもしれません。そのような世界でまたどういった革命が起きるのか楽しみです(OpenAIが現在進行形で革命を起こしていますが)。2025年も最新情報をキャッチアップしつつそれを社内におもしろおかしく伝えられる仕事ができたら嬉しいなと思う年末でございました。

明日の投稿は・・・

明日は新卒1年目の千葉がベンチャー企業で働くうえで意識していた点や、将来やりたいことが見つかったプロセスなどについて語ってくれるそうです。 今後弊社のようなベンチャー企業に興味がある学生の方々や、就活に悩んでいる方、やりたいことが見つかってなくて困っている方々にとってとても有益な記事になっているかと思いますのでお楽しみに!!

WE ARE HIRING!!

現在ベーシックでは中途採用・26卒の新卒採用をオープン中です!!
AI × 職種(セールス、マーケターetc…) というキャリアを積みたい方にはうってつけの環境だと思いますので是非ともご検討いただけますと幸いです。



いいなと思ったら応援しよう!