Step 4: StreamlitでUI化しよう 🖥️: YouTube動画要約アプリを作ろう! | SkillhubAI(スキルハブエーアイ)

Step 4: StreamlitでUI化しよう 🖥️: YouTube動画要約アプリを作ろう!

このレッスンで学ぶこと

  • Streamlitで入力→処理→表示→ダウンロードの基本フロー
  • youtube-transcript-apiget_transcript(languages=[...]) で字幕を取得(日本語優先→英語)
  • OpenAI APIで日本語要約(英語は翻訳して日本語要約
  • 送受信のログ可視化(▶ Request / ✓ Response)

0. 準備(初回だけ)

  1. パッケージを入れる
pip install streamlit openai youtube-transcript-api

  1. OpenAI の APIキーを環境変数に設定
  • mac/Linux:
  export OPENAI_API_KEY="sk-...YOUR_KEY..."


  • Windows(PowerShell):
  setx OPENAI_API_KEY "sk-...YOUR_KEY..."


※ 設定後は新しいターミナルを開き直すと確実です。


以降は同じファイル app_youtube_summary.py上書きしながら育てるスタイルです。


Step 1:まずは画面を出す(超最小)

ねらい:まずは“動く”を確認。タイトルだけ出してOKです。

ファイル名: app_youtube_summary.py

import streamlit as st

st.set_page_config(page_title="YouTube→要約", page_icon="📝")
st.title("📝 YouTube 字幕 → 日本語要約(URL/IDだけ)")

st.write("まずは画面が出ればOK!")

実行

streamlit run app_youtube_summary.py

ここで学ぶこと

  • import streamlit as st は「画面を作る道具」を読み込む合図。
  • まずは“起動成功”が一番大事。小さな成功体験を積み重ねましょう🤗

Step 2:入力欄とボタン(まだ要約しない)

ねらい:URL/IDを受け取り、ボタンで反応するを作ります。

import streamlit as st

st.set_page_config(page_title="YouTube→要約", page_icon="📝")
st.title("📝 YouTube 字幕 → 日本語要約(URL/IDだけ)")

video_input = st.text_input(
    "YouTubeのURL または 動画ID",
    placeholder="例) https://www.youtube.com/watch?v=LxvErFkBXPk / LxvErFkBXPk"
)
if st.button("テスト"):
    st.write("あなたの入力:", video_input)

ポイント

  • st.text_input は一行入力。
  • st.button は押された瞬間だけ中が動きます。
  • 入力→表示まで通ればOK!

Step 3:動画IDを取り出す(URLでも生IDでもOK)

ねらい:YouTubeの字幕APIは動画IDで呼びます。URLからIDを抜く関数を作りましょう。

import re
import streamlit as st

st.set_page_config(page_title="YouTube→要約", page_icon="📝")
st.title("📝 YouTube 字幕 → 日本語要約(URL/IDだけ)")

def extract_video_id(s: str) -> str | None:
    """URLでもIDでも受け取って動画IDらしきものを返す"""
    if not s:
        return None
    s = s.strip()
    m = re.search(r"v=([0-9A-Za-z_-]{11})", s) or re.search(r"youtu\.be/([0-9A-Za-z_-]{11})", s)
    if m:
        return m.group(1)
    if re.fullmatch(r"[0-9A-Za-z_-]{10,}", s):  # 生IDらしい文字列も許容
        return s
    return None

video_input = st.text_input("YouTubeのURL または 動画ID")
if st.button("IDを確認"):
    st.write("抽出した動画ID:", extract_video_id(video_input))

ポイント

  • re.search文字パターンで探す道具(正規表現)。
  • v=XXXXXXXXXXXyoutu.be/XXXXXXXXXXX11文字を拾っています。
  • ここで 正しくIDが取れる ことを先に確認。

Step 4:字幕を取得してプレビュー(あなたの get_transcript で)

ねらい:いよいよ字幕を取ります。あなたが前に使った形をそのまま使います。

import re
import streamlit as st
from youtube_transcript_api import YouTubeTranscriptApi, NoTranscriptFound, TranscriptsDisabled

st.set_page_config(page_title="YouTube→要約", page_icon="📝")
st.title("📝 YouTube 字幕 → 日本語要約(URL/IDだけ)")

def extract_video_id(s: str) -> str | None:
    if not s:
        return None
    s = s.strip()
    m = re.search(r"v=([0-9A-Za-z_-]{11})", s) or re.search(r"youtu\.be/([0-9A-Za-z_-]{11})", s)
    if m:
        return m.group(1)
    if re.fullmatch(r"[0-9A-Za-z_-]{10,}", s):
        return s
    return None

video_input = st.text_input("YouTubeのURL または 動画ID")
if st.button("字幕を取得(テスト)"):
    vid = extract_video_id(video_input)
    if not vid:
        st.error("URL/IDの形式を確認してください。")
    else:
        try:
            segments = YouTubeTranscriptApi.get_transcript(
                vid,
                languages=['ja', 'ja-JP', 'en', 'en-US', 'en-GB']  # 日本語優先→英語
            )
            preview = " ".join(seg.get("text", "") for seg in segments[:20])
            st.write(preview[:400] + ("..." if len(preview) > 400 else ""))
        except NoTranscriptFound:
            st.error("字幕が見つかりません(日本語/英語)。")
        except TranscriptsDisabled:
            st.error("この動画は字幕が無効化されています。")
        except Exception as e:
            st.error(f"字幕取得エラー: {e}")

ポイント

  • 戻り値は辞書リスト{'text': '...', 'start': ..., 'duration': ...})。ここでは text だけ連結してプレビュー。
  • まずは先頭だけ表示して“取れてるか”を確認しましょう。

Step 5:要約プロンプトを作る(日本語/英語で出し分け)

ねらい:日本語ならそのまま要約、英語なら翻訳してから要約。 「何をしてほしいか」を自然文で書くのがプロンプトです。

import re
import streamlit as st
from youtube_transcript_api import YouTubeTranscriptApi

st.set_page_config(page_title="YouTube→要約", page_icon="📝")
st.title("📝 YouTube 字幕 → 日本語要約(URL/IDだけ)")

def make_prompt_from_segments(segments: list[dict]) -> tuple[str, str]:
    """字幕からプロンプトを作る。返り値: (prompt, lang_hint)"""
    text = " ".join(seg.get("text", "") for seg in segments if seg.get("text"))
    # 日本語の文字が入っていれば日本語(ざっくり)
    is_ja = re.search(r"[\u3040-\u30ff\u4e00-\u9fff]", text) is not None
    src = text[:12000]  # まずは安定重視で上限
    if is_ja:
        return (
            "次の文章を日本語で300〜500字に要約し、最後に重要ポイントを3〜5個の箇条書きで示してください。\n"
            "専門用語は噛み砕いて、初学者にも分かる説明にしてください。\n\n"
            f"=== 元テキスト ===\n{src}\n"
        ), "ja"
    else:
        return (
            "次の英語テキストの内容を日本語に翻訳し、全体を300〜500字で分かりやすく要約してください。"
            "最後に重要ポイントを3〜5個、短い日本語の箇条書きで示してください。"
            "訳語は専門用語を避け、初学者に伝わる平易な表現を優先してください。\n\n"
            f"=== Source (EN) ===\n{src}\n"
        ), "en"

ポイント

  • プロンプト=お願い文。人に頼むのと同じで、丁寧に具体的に書くほど思い通りに。
  • まずは [:12000] で長さを抑えて“動く芯”を作ります(後でロング対応もOK)。

Step 6:OpenAIで要約して表示(最小)

ねらい:要約を画面に出せるようにします。

from openai import OpenAI
import re
import streamlit as st
from youtube_transcript_api import YouTubeTranscriptApi, NoTranscriptFound, TranscriptsDisabled

st.set_page_config(page_title="YouTube→要約", page_icon="📝")
st.title("📝 YouTube 字幕 → 日本語要約(URL/IDだけ)")

def extract_video_id(s: str) -> str | None:
    if not s: return None
    s = s.strip()
    m = re.search(r"v=([0-9A-Za-z_-]{11})", s) or re.search(r"youtu\.be/([0-9A-Za-z_-]{11})", s)
    if m: return m.group(1)
    if re.fullmatch(r"[0-9A-Za-z_-]{10,}", s): return s
    return None

def make_prompt_from_segments(segments: list[dict]) -> tuple[str, str]:
    text = " ".join(seg.get("text", "") for seg in segments if seg.get("text"))
    is_ja = re.search(r"[\u3040-\u30ff\u4e00-\u9fff]", text) is not None
    src = text[:12000]
    if is_ja:
        return (
            "次の文章を日本語で300〜500字に要約し、最後に重要ポイントを3〜5個の箇条書きで示してください。\n"
            "専門用語は噛み砕いて、初学者にも分かる説明にしてください。\n\n"
            f"=== 元テキスト ===\n{src}\n"
        ), "ja"
    else:
        return (
            "次の英語テキストの内容を日本語に翻訳し、全体を300〜500字で分かりやすく要約してください。"
            "最後に重要ポイントを3〜5個、短い日本語の箇条書きで示してください。"
            "訳語は専門用語を避け、初学者に伝わる平易な表現を優先してください。\n\n"
            f"=== Source (EN) ===\n{src}\n"
        ), "en"

video_input = st.text_input("YouTubeのURL または 動画ID")
if st.button("要約(表示のみ)"):
    vid = extract_video_id(video_input)
    if not vid:
        st.error("URL/IDの形式を確認してください。")
    else:
        try:
            segments = YouTubeTranscriptApi.get_transcript(
                vid,
                languages=['ja','ja-JP','en','en-US','en-GB']
            )
            prompt, lang = make_prompt_from_segments(segments)
            client = OpenAI()  # OPENAI_API_KEYは環境変数から自動取得
            with st.spinner("AIが要約しています..."):
                res = client.responses.create(model="gpt-4o-mini", input=prompt)
                summary = res.output_text
            st.subheader("要約(Markdown)")
            st.markdown(summary)
        except NoTranscriptFound:
            st.error("字幕が見つかりません(日本語/英語)。")
        except TranscriptsDisabled:
            st.error("この動画は字幕が無効化されています。")
        except Exception as e:
            st.error(f"エラー: {e}")

ポイント

  • OpenAI() は「AIにお願いする窓口」。APIキーは環境変数から自動取得。
  • client.responses.create(model=..., input=...)依頼。返事は res.output_text で本文だけ取得できます。

Step 7:ログ可視化(▶ Request / ✓ Response)

ねらい何を送り何が返って何秒かかったかを目で確認できるようにします。 あなたのバッチレッスンと同じ様式に揃えます。

import time
from openai import OpenAI
import re, streamlit as st
from youtube_transcript_api import YouTubeTranscriptApi

st.set_page_config(page_title="YouTube→要約", page_icon="📝")
st.title("📝 YouTube 字幕 → 日本語要約(URL/IDだけ)")

# ...(extract_video_id / make_prompt_from_segments は前ステップと同じ)

video_input = st.text_input("YouTubeのURL または 動画ID")
model = st.selectbox("使うモデル", ["gpt-4o-mini", "gpt-4.1-mini", "gpt-4.1"], index=0)

if st.button("要約(ログ付き)"):
    vid = extract_video_id(video_input)
    if not vid:
        st.error("URL/IDの形式を確認してください。")
    else:
        segments = YouTubeTranscriptApi.get_transcript(vid, languages=['ja','ja-JP','en','en-US','en-GB'])
        prompt, _ = make_prompt_from_segments(segments)

        # ▶ Request
        st.markdown("### ▶ Request")
        st.code(
            f"model: {model}\n"
            f"input length (chars): {len(prompt)}\n"
            f"prompt preview: {prompt[:120]}...",
            language="text",
        )

        # ✓ 呼び出し(1回だけ)+計測
        client = OpenAI()
        start = time.time()
        res = client.responses.create(model=model, input=prompt)
        elapsed = time.time() - start
        summary = res.output_text

        st.subheader("要約(Markdown)")
        st.markdown(summary)

        # ✓ Response
        with st.expander("✓ Response(詳細ログ)"):
            st.write(f"- response.id: `{getattr(res,'id',None)}`")
            st.write(f"- response.model: `{getattr(res,'model',None)}`")
            st.write(f"- elapsed: `{elapsed:.2f}s`")

ポイント

  • APIコールは1回のみ。ログで前後を挟むのがコツ。
  • st.codest.expander見やすい表示に。

Step 8:ダウンロードボタンで完成!🎉

ねらい:要約をMarkdownファイルで保存できると便利。

# (Step 7 の続き)
st.download_button(
    "要約をダウンロード(Markdown)",
    summary,
    file_name=f"{vid}_summary.md",
)

ポイント

  • st.download_buttonその場でファイル保存ができます。
  • 動画ID入りのファイル名にすると管理しやすい!

うまくいかないとき(まずここを見る)

  1. APIキーが効いていない
  • mac/Linux: echo $OPENAI_API_KEY
  • PowerShell: echo $Env:OPENAI_API_KEY
  • 新しいターミナルで実行しているか確認
  1. 字幕が取れない
  • 動画に字幕がない/非公開/地域制限/YouTube側の制限
  • 別動画で試す・時間を置く・ネットワークを変える
  1. 英語で返ってくる
  • プロンプトに「日本語で」の指示が入っているか(本レッスンのコードは入っています)
  1. 長文で遅い/落ちる
  • text[:12000] の数字を少し小さくして試す(まずは動かす)

仕上げ:完成版(参考)

ここまで育てた内容をひとつにまとまった形で置いておきます。 そのまま動かせます(あなたの get_transcript(languages=[...]) を採用、ログ様式も踏襲)。

# app_youtube_summary.py
import re
import time
import streamlit as st
from openai import OpenAI
from youtube_transcript_api import YouTubeTranscriptApi, NoTranscriptFound, TranscriptsDisabled

def extract_video_id(s: str) -> str | None:
    if not s:
        return None
    s = s.strip()
    m = re.search(r"v=([0-9A-Za-z_-]{11})", s) or re.search(r"youtu\.be/([0-9A-Za-z_-]{11})", s)
    if m:
        return m.group(1)
    if re.fullmatch(r"[0-9A-Za-z_-]{10,}", s):
        return s
    return None

def fetch_transcript_text(video_id: str) -> tuple[str | None, str | None, str | None]:
    try:
        segs = YouTubeTranscriptApi.get_transcript(
            video_id,
            languages=['ja', 'ja-JP', 'en', 'en-US', 'en-GB']
        )
        text = " ".join(seg.get("text", "") for seg in segs if seg.get("text"))
        lang_hint = "ja" if re.search(r"[\u3040-\u30ff\u4e00-\u9fff]", text) else "en"
        return text, lang_hint, None
    except NoTranscriptFound:
        return None, None, "字幕が見つかりません(日本語/英語)。"
    except TranscriptsDisabled:
        return None, None, "この動画は字幕が無効化されています。"
    except Exception as e:
        return None, None, f"字幕取得エラー: {e}"

def build_prompt(text: str, lang_hint: str) -> str:
    src = text[:12000]
    if lang_hint == "ja":
        return (
            "次の文章を日本語で300〜500字に要約し、最後に重要ポイントを3〜5個の箇条書きで示してください。\n"
            "専門用語は噛み砕いて、初学者にも分かる説明にしてください。\n\n"
            f"=== 元テキスト ===\n{src}\n"
        )
    else:
        return (
            "次の英語テキストの内容を日本語に翻訳し、全体を300〜500字で分かりやすく要約してください。"
            "最後に重要ポイントを3〜5個、短い日本語の箇条書きで示してください。"
            "訳語は専門用語を避け、初学者に伝わる平易な表現を優先してください。\n\n"
            f"=== Source (EN) ===\n{src}\n"
        )

def summarize_with_openai(prompt: str, model: str) -> tuple[str, dict]:
    client = OpenAI()
    start = time.time()
    res = client.responses.create(model=model, input=prompt)
    elapsed = time.time() - start
    meta = {
        "id": getattr(res, "id", None),
        "model": getattr(res, "model", None),
        "elapsed": elapsed,
        "usage": getattr(res, "usage", None),
    }
    return res.output_text, meta

st.set_page_config(page_title="YouTube→日本語要約", page_icon="📝")
st.title("📝 YouTube 字幕 → 日本語要約(URL/IDだけ)")

video_input = st.text_input(
    "YouTubeのURL または 動画ID",
    placeholder="例) https://www.youtube.com/watch?v=LxvErFkBXPk  または  LxvErFkBXPk"
)
model = st.selectbox("使うモデル", ["gpt-4o-mini", "gpt-4.1-mini", "gpt-4.1"], index=0)
go = st.button("要約する")

if go:
    vid = extract_video_id(video_input)
    if not vid:
        st.error("URL/IDの形式を確認してください。")
    else:
        with st.spinner("字幕を取得中..."):
            text, lang_hint, err = fetch_transcript_text(vid)

        if err:
            st.error(err)
        elif not text.strip():
            st.error("字幕テキストが空でした。")
        else:
            prompt = build_prompt(text, lang_hint)

            # ▶ Request
            st.markdown("### ▶ Request")
            st.code(
                f"model: {model}\n"
                f"input length (chars): {len(prompt)}\n"
                f"prompt preview: {prompt[:120]}...",
                language="text",
            )

            # ✓ 実行(1回だけ)
            with st.spinner("AIが要約しています..."):
                try:
                    summary, meta = summarize_with_openai(prompt, model=model)
                except Exception as e:
                    st.error(f"要約中にエラー: {e}")
                    st.stop()

            st.subheader("要約(Markdown)")
            st.markdown(summary)

            with st.expander("✓ Response(詳細ログ)"):
                st.write(f"- response.id: `{meta.get('id')}`")
                st.write(f"- response.model: `{meta.get('model')}`")
                st.write(f"- elapsed: `{meta.get('elapsed'):.2f}s`")
                usage = meta.get("usage")
                if usage:
                    in_t = getattr(usage, "input_tokens", None) or getattr(usage, "prompt_tokens", None)
                    out_t = getattr(usage, "output_tokens", None) or getattr(usage, "completion_tokens", None)
                    tot_t = getattr(usage, "total_tokens", None)
                    st.write(f"- tokens: in={in_t}, out={out_t}, total={tot_t}")
                else:
                    st.write("- tokens: (usage情報なし)")

            st.download_button(
                "要約をダウンロード(Markdown)",
                summary,
                file_name=f"{vid}_summary.md",
            )


🎊 おめでとうございます! これで 「YouTubeのURL/ID → 字幕取得 → 日本語要約(英語は翻訳して要約) → ログ可視化 → ダウンロード」 が一連で完成です。 例のごとく、まずは“動く芯”を作ってから、モデル切り替え、長文分割、プロンプト改善などに発展させていきましょう!