目次
- 目次
- はじめに
- Githubリポジトリ
- 使用技術
- 設計
- 実際にできある形
- Youtube API で動画情報を取得してきてDBに保存する
- 字幕情報を取得し、DBに保存する
- 字幕情報をLLMに渡して要約を生成してもらう
- Discordに要約を送信する
- まとめ
はじめに
最近、AIを使った開発をよく目にするため自分も何か作っておきたいと思っていました。
よく耳にするAIサービスのAPIを使うと費用が嵩んでしまうので、OllamaでローカルにLLMを立てて、Youtubeの動画を要約して、Discordのフォーラムチャンネルに送信するBotを作成してみようと思います。
特定のYoutubeチャンネルを監視して、新しい動画が出たら要約を生成、送信という形でやっていこうと思います。
Githubリポジトリ
使用技術
- Ollama
- Youtube API
- Discord.py
それぞれの環境構築方法はすでにたくさんあるので各自で調べてみてください。
設計
設計というほどがっつりは考えませんが、、どのように特定のチャンネルの動画の要約を実現するのかというと、、、
① Youtube API or Youtube RSSフィード で動画情報を取得
② 該当の動画の字幕を取得
③ LLMに要約してもらう
の流れでやっていこうと思います。
それぞれ取得した動画・字幕情報や、生成した要約データはDBに保存するようにもします。
YoutubeAPI 経由で動画情報を取得してくる場合
初回に使用します。
大半のチャンネルはすでに100本以上動画がありますが、
RSS では直近の動画しか持ってこれないので網羅することができません。
ですので、APIを使って全動画の動画を取得します。
RSS 経由で動画を取得してくる場合
2回目以降に使用します。
動画が投稿されたら拾ってきて要約を生成するというようにします。
字幕情報を持ってくる
こちらは、Youtube API で字幕情報は持ってくることは可能なのですが、
OAuth認証が必要となり、サーバー環境でやろうとすると少し手間になってしまいます。
ですので、別途ライブラリを使用し、OAuthを回避して字幕情報を取得しようと思います。
実際にできある形
構成図
フローチャート
最新の動画を取得して要約を生成するフロー
チャンネルの全動画を取得するフロー
要約をDiscordに投稿するフロー
Youtube API で動画情報を取得してきてDBに保存する
動画件数は結構な量があると思います。
ローカルLLMに字幕情報を渡すとしても全件見るのは時間がかかると思うので、DBに保存しておいて
好きなタイミングで要約生成ができるようにしておきましょう
DBを作成する
下記のようにクエリを作成して、実行します。
スキーマとテーブル作成
-- スキーマの作成
CREATE SCHEMA IF NOT EXISTS youtube_feed_summary;
-- チャンネル情報保存テーブル
CREATE TABLE IF NOT EXISTS youtube_feed_summary.channel (
channel_id VARCHAR(255) NOT NULL PRIMARY KEY,
channel_name VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 動画情報保存テーブル
CREATE TABLE IF NOT EXISTS youtube_feed_summary.video (
video_id VARCHAR(255) NOT NULL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
channel_id VARCHAR(255) NOT NULL,
published DATE NOT NULL,
link VARCHAR(255),
summary_send_flag BOOLEAN DEFAULT FALSE,
FOREIGN KEY (channel_id) REFERENCES youtube_feed_summary.channel (channel_id) ON DELETE CASCADE
);
-- 動画字幕情報保存テーブル
CREATE TABLE IF NOT EXISTS youtube_feed_summary.captions (
video_id VARCHAR(255) NOT NULL PRIMARY KEY,
caption TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (video_id) REFERENCES youtube_feed_summary.video (video_id) ON DELETE CASCADE
);
-- 要約情報保存テーブル
CREATE TABLE IF NOT EXISTS youtube_feed_summary.summary (
video_id VARCHAR(255) NOT NULL PRIMARY KEY,
summary TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (video_id) REFERENCES youtube_feed_summary.video (video_id) ON DELETE CASCADE
);
Youtube API から動画情報を取得する
ではまず、Youtube API から特定チャンネルの動画情報を取得してくるところから始めます。
Youtube動画にはそれぞれを識別するために videoId
が設定されています。
それらをキーとして、字幕情報取得、DB保存をやっていこうと思います。
以下コード↓
指定したチャンネルから全件動画情報を持ってくるスクリプト
from googleapiclient.discovery import build
API_KEY = "~~~~"
youtube = build("youtube", "v3", developerKey=API_KEY)
def get_all_videos(channel_id):
# チャンネルの全動画を取得
playlist_id = get_uploads_playlist_id(channel_id)
all_videos = get_all_videos_from_playlist(playlist_id)
return all_videos
def get_uploads_playlist_id(channel_id):
response = youtube.channels().list(
part="contentDetails",
id=channel_id
).execute()
return response["items"][0]["contentDetails"]["relatedPlaylists"]["uploads"]
def get_all_videos_from_playlist(playlist_id):
videos = []
next_page_token = None
while True:
response = youtube.playlistItems().list(
part="snippet",
playlistId=playlist_id,
maxResults=50,
pageToken=next_page_token
).execute()
for item in response["items"]:
videos.append({
"video_id": item["snippet"]["resourceId"]["videoId"],
"title": item["snippet"]["title"],
"published": item["snippet"]["publishedAt"],
"link": f'https://www.youtube.com/watch?v={item["snippet"]["resourceId"]["videoId"]}'
})
next_page_token = response.get("nextPageToken")
if not next_page_token:
break
return videos
こんな感じで指定したチェンネルの動画をすべて持ってきます。
最初は youtube.channnels()
からそのまま動画情報を持ってこようとしたのですが、数件数が合わず、、、
調べていく内に、playlist
から持ってきたら全件持ってくることができるとのことだったのでそちらに修正しました。
DBに保存する
DB操作関連をまとめたクラスを作っておきます。
DB操作をまとめたクラス
import psycopg2
class DBManager():
def __init__(self):
self.connection = None
self.cursor = None
# 接続情報
self.DB_URL = "~~~"
self._connect()
def _connect(self):
self.connection = psycopg2.connect(self.DB_URL)
self.cursor = self.connection.cursor()
def _close(self):
if self.cursor:
self.cursor.close()
if self.connection:
self.connection.close()
def save_new_data(self, data, channel_id, channel_name):
try:
# データをvideoテーブルに挿入
for video in data:
# video テーブル
self.cursor.execute(
"INSERT INTO youtube_feed_summary.video (video_id, title, channel_id, published, link) VALUES (%s, %s, %s, %s, %s)",
(video['video_id'], video['title'], channel_id,
video['published'], video['link'])
)
# captions テーブル
self.cursor.execute(
"INSERT INTO youtube_feed_summary.captions (video_id) VALUES (%s)",
(video['video_id'],)
)
# summary テーブル
self.cursor.execute(
"INSERT INTO youtube_feed_summary.summary (video_id) VALUES (%s)",
(video['video_id'],)
)
self.connection.commit()
except Exception as e:
print("Error:", e)
self.connection.rollback()
print("Rollback executed")
finally:
self.cursor.close()
self.connection.close()
字幕情報を取得し、DBに保存する
字幕情報を取得します。
Youtube API を使用しても取得可能なのですが、先述したようにOAuthを通さないといけないので面倒です。
ドキュメント:https://developers.google.com/youtube/v3/guides/implementation/captions?hl=ja
ですので別途ライブラリを使用します。それが youtube-transcript-api
です。
Githubリポジトリ:https://github.com/jdepoix/youtube-transcript-api
こちらを使用すれば、OAuthを通さず、videoId
を渡すだけで字幕情報を取得することができます。
以下コード↓
動画IDから字幕情報を持ってくるスクリプト
from youtube_transcript_api import YouTubeTranscriptApi
def get_caption(video_id):
# 対象のYouTube動画のIDを指定
# 日本語の字幕を取得
transcript = YouTubeTranscriptApi.get_transcript(video_id, languages=['ja'])
# 字幕テキストを結合して一つの文字列にする
transcript_text = ' '.join([item['text'] for item in transcript])
return transcript_text
取得した字幕をDBに保存する
こちらは、先ほど書いた、DBManager
クラスに追加します。
def save_caption_data(video_id, caption):
try:
conn = psycopg2.connect(DB_URL)
cur = conn.cursor()
# 字幕データを保存
cur.execute(
"UPDATE youtube_feed_summary.captions SET caption = %s, updated_at = NOW() WHERE video_id = %s",
(caption, video_id)
)
conn.commit()
except Exception as e:
print("Error:", e)
conn.rollback()
finally:
cur.close()
conn.close()
字幕情報をLLMに渡して要約を生成してもらう
Ollama を用いて要約を生成してもおうと思います。
環境構築等はすでに多くの記事が回っていると思いますのでそちらを参考にしていただければと思います。
モデルによっては日本語を全然返してくれないモデルもアルので日本語に対応しているモデルを探しました。
使用する人によって変わると思いますが、今回は tanuki-dpo-v1.0
を使用しました。
URL:https://ollama.com/7shi/tanuki-dpo-v1.0
[!NOTE]
こちらは作成したあとに気づいたのですが、上記のURLにはこのように記載されています。
!!!!注意!!!!
本モデルは、性能低下のため非推奨となっているGGUF版を使用しています。性能低下を踏まえた上で、体験版として提供するものです。
NFKC正規化がサポートされていないため、全角英数字や半角カナが認識できません。入力しないようにご注意ください。
とあるため他のモデルを使用したほうがいいかもしれません。
こちらで要約を生成していきます。
今回は Modelfile
を作成して、Youtubeの動画要約に特化させるように事前にプロンプトを渡しておこうと思います。
Modelfile
FROM 7shi/tanuki-dpo-v1.0:latest
SYSTEM """
あなたは優秀な日本語の長文要約エキスパートです。
以下に示す「YouTube動画の書き起こし全文」を読み、その内容を「約5,000文字程度の日本語」で丁寧に要約してください。
【必須条件】
- 書き起こしの中で話者が特に強調している主張やメッセージを中心に要約してください。
- 細かすぎるエピソードや繰り返される表現は省略または簡潔化し、要点が明確に伝わるようにしてください。
- 元の文脈やニュアンスを損なわないよう注意し、話者の意図や感情も適切に表現してください。
【出力構成】
# 要約内容
要約した内容をタイトルとして記載します。
## はじめに
- 話者が視聴者に伝えたいこと、話し始めた動機、背景を1000文字程度で整理します。
## 主なメッセージと重要なアドバイス
- 話者が具体的に語っている内容を整理し、項目ごとに適切な小見出しを付けてください。
- ポイントごとに簡潔かつ明快に5000文字程度に要約してください。
## おわりに
- 話者が視聴者に最後に伝えたいことや、励ましのメッセージを1000文字程度でまとめます。
"""
PARAMETER temperature 0.2
PARAMETER top_p 0.7
PARAMETER num_ctx 8192
ollama create youtube-summary-model -f Modelfile
でモデルをビルドします。
これでカスタムモデルを作成して、 ollama に登録することができます。
Discordに要約を送信する
公開は普通のチャンネルではなく、フォーラムチャンネルに送信しようと思います。
普通のチャンネルだと流れてしまいますが、フォーラムに送ることで全部がスレッドになり、
要約したものを見返しやすいと思ったからです。
Discord.py を使用して、要約を送信していきます。
今回は特にコマンドを指定するとかはなく、cron で定期的に起動させて、
要約を送信する形にしようと思います。
要約をDiscordに送信するスクリプト
main.py
import discord
from classes import youtube_summary_bot
from utils.config import load_env
def main():
load_env()
intents = discord.Intents.default()
intents.message_content = True
bot = youtube_summary_bot.YoutubeSummaryBot(intents=intents)
bot.run(bot.token)
if __name__ == "__main__":
main()
bot クラス
import os
import discord
from classes.database_manager import DatabaseManager
class YoutubeSummaryBot(discord.Client):
def __init__(self, intents: discord.Intents):
super().__init__(intents=intents)
self.token = os.getenv("DISCORD_TOKEN")
self.forum_channel_id = os.getenv("FORUM_CHANNEL_ID")
if self.token is None:
raise ValueError(
"DISCORD_TOKEN is not set in the environment variables.")
async def on_ready(self):
print(f'Logged in as {self.user} (ID: {self.user.id})')
print('------')
await self.get_summary()
async def on_message(self, message):
if message.author == self.user:
return
async def get_summary(self):
db_manager = DatabaseManager()
summary_data = db_manager.get_not_send_summaries_data()
if not summary_data:
return
await self._send_summary_message_to_forum(summary_data)
db_manager.update_summary_send_flag(summary_data[0][3])
db_manager._close()
async def _send_summary_message_to_forum(self, data):
forum_channel = self.get_channel(self.forum_channel_id)
if isinstance(forum_channel, discord.ForumChannel):
title = f"【要約】{data[0][0]}"
content = f"{data[0][1]} \n\n{data[0][2]}"
await forum_channel.create_thread(
name=title,
content=content,
auto_archive_duration=10080
)
else:
print("指定されたチャンネルはフォーラムチャンネルではありません。")
実際に送信されるメッセージ
今回は、要約がイラストの添削や、リスナーからの相談を受け付けているさいとうなおき先生の動画をお借りして試してみます。
今回要約する動画がこちら↓
URL:https://www.youtube.com/watch?v=1xp-o1jksSY
そして要約したものがこちら↓
【要約内容】
斎藤直樹さんは、YouTubeチャンネル「斎藤直樹のイラスト講座」でリスナーから寄せられた質問に丁寧に回答しています。今回の質問は、40代のAさんがイラストレーターやポスター制作を目指しているが、時間がないと感じているというものでした。斎藤さんはまず、Aさんの状況に対して共感し、励ましのメッセージを送ります。
斎藤さんは、Aさんが絵を描くことに情熱を持っていることを評価し、無理に完璧なスタイルを追求するのではなく、まずは自分が楽しめる範囲で始めることが重要だとアドバイスします。具体的には、1日1時間程度の制作時間を確保し、その中で様々なジャンルや技法に挑戦することを勧めます。また、SNSやオンラインコミュニティを活用して他のクリエイターと交流し、フィードバックを得ることも有益だと述べています。
さらに、斎藤さんはAさんが過去に経験した挫折や困難も理解し、それを乗り越えるための具体的なステップを提案します。例えば、簡単なテストプロジェクトを通じてスキルを磨きつつ、徐々に複雑な作品に挑戦していくことが効果的だと説明します。また、Aさんが描くキャラクターや背景に対するフィードバックを積極的に求めることで、自分のスタイルを確立しやすくなるとアドバイスしています。
最後に、斎藤さんはAさんが諦めずに努力を続けることの大切さを強調し、応援していることを伝えます。このメッセージは、Aさんにとって大きな励みとなるでしょう。
【出力構成】
# 要約内容
斎藤直樹さんはYouTubeチャンネル「斎藤直樹のイラスト講座」でリスナーから寄せられた質問に丁寧に回答しています。今回の質問は、40代のAさんがイラストレーターやポスター制作を目指しているが、時間がないと感じているというものでした。斎藤さんはまずAさんの状況に対して共感し、励ましのメッセージを送ります。
斎藤さんは、Aさんが絵を描くことに情熱を持っていることを評価し、無理に完璧なスタイルを追求するのではなく、まずは自分が楽しめる範囲で始めることが重要だとアドバイスします。具体的には、1日1時間程度の制作時間を確保し、その中で様々なジャンルや技法に挑戦することを勧めます。また、SNSやオンラインコミュニティを活用して他のクリエイターと交流し、フィードバックを得ることも有益だと述べています。
さらに、斎藤さんはAさんが過去に経験した挫折や困難も理解し、それを乗り越えるための具体的なステップを提案します。例えば、簡単なテストプロジェクトを通じてスキルを磨きつつ、徐々に複雑な作品に挑戦していくことが効果的だと説明します。また、Aさんが描くキャラクターや背景に対するフィードバックを積極的に求めることで、自分のスタイルを確立しやすくなるとアドバイスしています。
最後に、斎藤さんはAさんが諦めずに努力を続けることの大切さを強調し、応援していることを伝えます。このメッセージは、Aさんにとって大きな励みとなるでしょう。
# 終わりに
斎藤直樹さんのアドバイスと励ましの言葉は、Aさんにとって非常に有益であり、イラストレーターとしての道を進むための具体的な指針となるでしょう。時間がない中でも、楽しみながら学び続けることで、必ず成長できるという信念が伝わってきます。
https://www.youtube.com/watch?v=1xp-o1jksSY
いい感じに要約してくれていますね!
懸念
やはり、要約部分をAIというブラックボックスに投げているので、どうしても意図していない要約が返されることもあります。
例えば以下のような要約です。
要約失敗の例
このYoutube動画は、Aさんのイラストについての質問に対して回答しています
特に強調されているポイントやアドバイスを以下のように要約します。
(5000文字)
5000文字に要約してと送ったら、5000文字と返してくれました。笑
ゲームをやっている裏で要約を動かしたらこのようになったのでおそらくメモリ不足だったと思いますが、
安定的に要約を生成するのであれば、APIを素直に使用したほうがいいかもしれませんね。
まとめ
今回は、Youtubeの動画を要約してDiscordに送信するBotを作成してみました。
Ollamaを使用することで、ローカルでLLMを立てることができ、APIのコストを気にせずに要約を生成することができました。
ただし、安定性や精度を求めるのであれば、APIを使用したほうがいいかもしれませんね。