🦩

【初Python】Flutter公式のYoutubeを日本語で要約してNotionにメモする仕組みを作ってみた

に公開

はじめに

最近、Flutterの公式Youtubeチャンネルで「How Flutter Works」シリーズみたいな動画が連日アップされてて、「これは勉強になりそうだから見ておかないと」という気持ちと、「英語の動画は疲れるな」という気持ちが戦って、結局1本も見てません...

「これではいけない!」と思い、まずは日本語で要約したものに目を通してから、Flutter公式の動画に臨むという手順でいこうと考えました。

そこで、LLMと相性が良いというPythonを人生で初めて使い、Flutter公式のYoutubeを日本語で要約し、Notionにメモする仕組みを作ってみました。

Pythonの素人として、同じくPython勉強中の人たちのお役に立てることを願って、記事を書いておきます。

概要

ざっくり手順は以下の通りです。

  1. プレイリストのIDからプレイリスト内の全ての動画IDを取得する
  2. 動画の字幕を英語で取得する
  3. 取得した字幕を英語で要約する
  4. 要約を日本語に翻訳する
  5. Notionのデータベースに追加する

YoutubeのプレイリストのIDを指定して、そのプレイリスト内の動画の日本語要約をNotionにメモするという仕組みを作ってます。

Notionには動画ID・タイトル・URL・視聴済み(チェックボックス)・要約をメモします。

各APIの仕様や料金を確認したかったので、とりあえず一連の動作が確認できるものっていう感じです。

今後はGCPのCloud Functionsで1日1回実行し、Flutter公式のチャンネルに新しい動画があれば自動的にNotionにメモされるような仕組みを作る予定です。

ページ最後に「これからのタスク」としてまとめて、順次記事を追加していこうと思います。

準備

新しいディレクトリを作成し、準備していきます。
とりあえず、以下ファイルとディレクトリを作成していきます。

  • main.py
  • app/(ディレクトリ)
  • requirements.txt
  • .env

appディレクトリにはこのあと個別の処理を記載したファイルを作成していきます。

また、Pythonにはvenvという仮想環境を作る便利ツールがあるようで、以下コマンドで仮想環境を作成します。

Mac環境

source venv/bin/activate

Windows環境

venv\Scripts\activate

ここまでで以下のディレクトリができていればOKです!

your-project/
├── main.py
├── app/
├── requirements.txt
├── .env
└── venv/

プレイリストのIDからプレイリスト内の全ての動画IDを取得する

この処理では「YouTube Data API v3」を利用します。
私が調べた限り無料なようですが、1日の利用回数に制限があるようです。
https://developers.google.com/youtube/v3/getting-started?hl=ja

APIキーの取得方法などはWEB上にたくさんの記事がありますので、そちらを参考にしてください。
取得したAPIキーは「YOUTUBE_API_KEY」として.envファイルに記載してください。

YouTube Data API v3を利用するために以下コマンドを実行します。

pip install google-api-python-client

次に、appディレクトリ内に以下のファイルを作成します。

app/youtube_client.py
import isodate
from googleapiclient.discovery import build
from app.config import YOUTUBE_API_KEY


youtube = build("youtube", "v3", developerKey=YOUTUBE_API_KEY)

# プレイリスト内の全ての動画IDを取得
def get_video_ids_in_playlist(playlist_id):
    video_ids = []
    next_page = None
    while True:
        res = youtube.playlistItems().list(
            part="contentDetails",
            playlistId=playlist_id,
            maxResults=50,
            pageToken=next_page
        ).execute()
        for item in res["items"]:
            video_ids.append(item["contentDetails"]["videoId"])
        next_page = res.get("nextPageToken")
        if not next_page:
            break
    return video_ids

# ショート動画を除外
def filter_out_shorts(video_ids):
    filtered = []
    for i in range(0, len(video_ids), 50):  # APIは最大50件ずつ
        chunk = video_ids[i:i+50]
        res = youtube.videos().list(
            part="contentDetails,snippet",
            id=",".join(chunk)
        ).execute()
        for item in res["items"]:
            duration = isodate.parse_duration(
                item["contentDetails"]["duration"])
            title = item["snippet"]["title"]
            if duration.total_seconds() >= 60:  # 60秒未満はショートと判断
                filtered.append({
                    "videoId": item["id"],
                    "title": title,
                    "duration": duration.total_seconds()
                })
    return filtered

# プレイリスト内のショート動画を除外した動画IDを取得
def get_videos_in_playlist_without_shorts(playlist_id):
    video_ids = get_video_ids_in_playlist(playlist_id)
    filtered_videos = filter_out_shorts(video_ids)
    return filtered_videos

ショート動画は取得したくないので、60秒未満の動画はショート動画とみなして、除外することにしました。
ショート動画をきちんと判別する方法を見つけたら、ここは修正したいところです!

これで、get_videos_in_playlist_without_shorts(playlist_id)を実行すれば、プレイリスト内の動画IDを全て取得できるようになりました。

動画の字幕を英語で取得する

ここでは「youtube-transcript-api」を利用します。
非公式らしいのですが、無料で使い勝手のいいAPIです。

以下コマンドでインストールします。

pip install youtube-transcript-api

次に、appディレクトリ内に以下のファイルを作成します。

app/transcript.py
from youtube_transcript_api import YouTubeTranscriptApi

def get_transcript(video_id):
    try:
        # 字幕一覧を取得
        transcript_list = YouTubeTranscriptApi.list_transcripts(video_id)

        # 英語の字幕があるか確認
        if "en" not in [t.language_code for t in transcript_list]:
            print(f"警告: 動画 {video_id} には英語字幕(en)が存在しません。")
            return None

        # 字幕を取得(英語字幕)
        transcript = YouTubeTranscriptApi.get_transcript(
            video_id, languages=['en'])

        # 字幕をテキストに変換
        transcript_text = " ".join(entry['text'] for entry in transcript)
        return transcript_text

    except Exception as e:
        print(f"字幕の取得に失敗しました: {e}")
        return None

これで、get_transcript(video_id)を実行するとYoutubeの字幕を取得して、テキストとして出力できるようになりました。

取得した字幕を英語で要約する→日本語に翻訳する

OpenAIのAPIを利用して字幕を要約し、翻訳する処理を作成していきます。
APIキーの取得方法はWEB上に記事がたくさんありますので、そちらを参考にしてください。
取得したAPIキーは「OPENAI_API_KEY」として.envに記載しておいてください。

以下コマンドでOpenAIを使うためのライブラリをインストールします。

pip install openai

次に、appディレクトリ内に以下のファイルを作成します。

app/summarizer.py
import textwrap
from openai import OpenAI
from app.config import OPENAI_API_KEY

# 環境変数からAPIキーを取得
client = OpenAI(api_key=OPENAI_API_KEY)

# 英語字幕を要約
def summarize_in_english(text: str, chunk_size: int = 2000) -> str:
    if not text or text.strip() == "":
        return "No transcript available to summarize."
        
    chunks = textwrap.wrap(text, width=chunk_size)
    partial_summaries = []

    for chunk in chunks:
        prompt = f"Summarize the following transcript in 3-5 bullet points:\n\n{chunk}"
        response = client.chat.completions.create(
            model="gpt-3.5-turbo",
            messages=[{"role": "user", "content": prompt}],
            temperature=0.3,
        )
        partial_summaries.append(response.choices[0].message.content.strip())

    # 全体の段階的要約
    final_prompt = "Based on the following bullet points, write a concise summary:\n\n" + "\n\n".join(partial_summaries)
    final_response = client.chat.completions.create(
        model="gpt-3.5-turbo",
        messages=[{"role": "user", "content": final_prompt}],
        temperature=0.3,
    )
    return final_response.choices[0].message.content.strip()

# 要約を日本語に翻訳
def translate_to_japanese(summary: str) -> str:
    prompt = f"以下の英語の要約を自然な日本語に翻訳してください:\n\n{summary}"
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[{"role": "user", "content": prompt}],
        temperature=0.3,
    )
    return response.choices[0].message.content.strip()

# 全体フロー
def summarize_and_translate(text: str) -> str:
    english_summary = summarize_in_english(text)
    japanese_summary = translate_to_japanese(english_summary)
    return japanese_summary

OpenAIのAPIはトークンごとに料金がかかり、英語で要約→日本語に翻訳した方が節約できそうな気がしたので、この流れにしました。

Notionのデータベースに追加する

Notion APIインテグレーションを使って、翻訳した内容をNotionに追加していきます。

この処理では、NotionインテグレーションToken(APIキー)とNotionへのコネクトが必要になります。
以下の記事がわかりやすかったので参考にしてください。
https://temp.co.jp/blog/2024-01-21-notion-integration-connect

また、Notion内でデータベースを作成し、データベースIDを取得します。
以下の記事を参考に取得してください。
https://booknotion.site/setting-databaseid

APIキーは「NOTION_API_KEY」として、データベースIDは「NOTION_DATABASE_ID」として.envに記載します。

データベースには以下のプロパティを作成しておきます。

  • ID
  • タイトル
  • URL
  • 視聴済み

以下コマンドでNotionを操作するライブラリをインストールします。

pip install notion-client

次に、appディレクトリ内に以下のファイルを作成します。

app/notion_uploader.py
from notion_client import Client
from app.config import NOTION_API_KEY, NOTION_DATABASE_ID
from typing import Optional

notion = Client(auth=NOTION_API_KEY)

# 1. ページ作成(プロパティのみ)
def upload_to_notion(video_id, title, summary=None):
    page = notion.pages.create(
        parent={"database_id": NOTION_DATABASE_ID},
        properties={
            "ID": {
                "rich_text": [
                    {
                        "text": {
                            "content": video_id
                        }
                    }
                ]
            },
            "タイトル": {
                "title": [
                    {
                        "text": {
                            "content": title
                        }
                    }
                ]
            },
            "URL": {
                "url": f"https://www.youtube.com/watch?v={video_id}"
            },
            "視聴済み": {
                "checkbox": False
            }
        }
    )

    # 2. ページIDを取得して本文を追加
    page_id = page["id"]

    notion.blocks.children.append(
        block_id=page_id,
        children=[
            {
                "object": "block",
                "type": "paragraph",
                "paragraph": {
                    "rich_text": [
                        {
                            "type": "text",
                            "text": {
                                "content": summary if summary else "要約はありません。",
                            },
                        }
                    ]
                }
            }
        ]
    )

引数summaryにデフォルト引数を設定しているのは、OpenAIのAPIを実行しなくてもNotionへの追加を確認できるように設定したかったからです。

次の手順で作成するmain.pyでOpenAIのAPIを実行を設定できるようにすることで、テスト実行するときはOpenAIを除いた無料のAPIのみ実行されるようにしています。

main.pyの作成

ここまでの手順で個別の処理は完成しました。
次に個別の処理を合わせて実行するmain.pyを作成します。

main.py
from app.transcript import get_transcript
from app.summarizer import summarize_and_translate
from app.youtube_client import get_videos_in_playlist_without_shorts
from app.notion_uploader import upload_to_notion

GENERATE_SUMMARY = False

playlist_id = "PLjxrf2q8roU28W3pXbISJbVA5REsA41Sx"  # Flutter公式のプレイリスト

videos = get_videos_in_playlist_without_shorts(playlist_id)
for video in videos:
    print(f"\n処理開始: {video['title']} (ID: {video['videoId']})")
    
    transcript = get_transcript(video["videoId"])
    if transcript:
        try:
            if GENERATE_SUMMARY:
                summary = summarize_and_translate(transcript)
                upload_to_notion(video["videoId"], video["title"], summary)
            else:
                upload_to_notion(video["videoId"], video["title"])
            print(f"動画{video['videoId']}の要約をNotionにアップロードしました")
        except Exception as e:
            print(f"エラー: 動画 {video['videoId']} の要約またはアップロード中に問題が発生しました: {e}")
    else:
        print(f"エラー: 動画 {video['videoId']} の字幕を取得できませんでした")

playlist_idには要約を作成したいプレイリストのIDを指定します。

また、OpenAIのAPIを使って要約したい場合は、GENERATE_SUMMRYをTrueに変更します。
OpenAIのAPIは有料なので翻訳・要約以外のテストをしたい場合はGENERATE_SUMMRYをFalseにして実行すると節約になります!

その他ファイルの作成

config.pyの作成

環境変数を管理するconfig.pyを作成します。

app/config.py
import os
from dotenv import load_dotenv

load_dotenv()

OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
NOTION_API_KEY = os.getenv("NOTION_API_KEY")
NOTION_DATABASE_ID = os.getenv("NOTION_DATABASE_ID")
YOUTUBE_API_KEY = os.getenv("YOUTUBE_API_KEY")

これは各ファイルで毎回load_dotenv()を呼ぶのが気持ち悪かったのと、CloudFunctionsにデプロイした時に.envから直接読み込むことはない想定なので作成しておきました。

デプロイするときはSecret Managerを使う予定で、環境変数の管理はその時にまた考えます...

init.pyの作成

appディレクトリに__init__.pyという名前の空ファイルを作成するだけです。
これは正直必須なのかどうかはわかりませんが、appディレクトリがパッケージとして認識されるらしいです。

いざ、実行!!

実行前の最終確認だけしておきましょう。
ここまでの手順が完了すれば、ディレクトリは以下のようになっているはずです。

├── main.py
├── app
│   ├── __init__.py
│   ├── config.py
│   ├── notion_uploader.py
│   ├── summarizer.py
│   ├── transcript.py
│   └── youtube_client.py
├── .env
└── requirements.txt

また、.envには以下のキーが設定されているはずです。

.env
YOUTUBE_API_KEY=
OPENAI_API_KEY=
NOTION_API_KEY=
NOTION_DATABASE_ID=

ここまで問題なければ、以下のコマンドを実行し、処理開始です!

python main.py

Notionのデータベースにプレイリスト内の動画のメモが追加されていれば成功です👌

今後はGCPのCloud Functionsで1日1回実行し、Flutter公式のチャンネルに新しい動画があれば自動的にNotionにメモされるような仕組みを作る予定です!

これからのタスク

  • Notionにメモする内容の追加
    • プレイリスト名も追加する(Notion側でフィルターができて便利)
    • 日本語で1行の概要も追加する(Notion側のテーブルビューで内容がすぐに分かるように)
  • 要約を読みやすくする
    • 要約を構造化する(目的・結論など)
    • 要約テキストをNotionの書式に合わせて出力する
      → AIが出力してくれる要約テキストの出力形式をどのようにすればいいのか考え中
  • Cloud Functionsでの定期実行
    • 処理の前にNotionにメモ済みの動画IDを取得し、要約の対象を抽出する

Discussion