このレッスンで学ぶこと
- Streamlitで入力→処理→表示→ダウンロードの基本フロー
youtube-transcript-api
のget_transcript(languages=[...])
で字幕を取得(日本語優先→英語)- OpenAI APIで日本語要約(英語は翻訳して日本語要約)
- 送受信のログ可視化(▶ Request / ✓ Response)
0. 準備(初回だけ)
- パッケージを入れる
pip install streamlit openai youtube-transcript-api
- 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=XXXXXXXXXXX
やyoutu.be/XXXXXXXXXXX
の11文字を拾っています。- ここで 正しく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.code
とst.expander
で見やすい表示に。
Step 8:ダウンロードボタンで完成!🎉
ねらい:要約をMarkdownファイルで保存できると便利。
# (Step 7 の続き)
st.download_button(
"要約をダウンロード(Markdown)",
summary,
file_name=f"{vid}_summary.md",
)
ポイント
st.download_button
はその場でファイル保存ができます。- 動画ID入りのファイル名にすると管理しやすい!
うまくいかないとき(まずここを見る)
- APIキーが効いていない
- mac/Linux:
echo $OPENAI_API_KEY
- PowerShell:
echo $Env:OPENAI_API_KEY
- 新しいターミナルで実行しているか確認
- 字幕が取れない
- 動画に字幕がない/非公開/地域制限/YouTube側の制限
- 別動画で試す・時間を置く・ネットワークを変える
- 英語で返ってくる
- プロンプトに「日本語で」の指示が入っているか(本レッスンのコードは入っています)
- 長文で遅い/落ちる
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 → 字幕取得 → 日本語要約(英語は翻訳して要約) → ログ可視化 → ダウンロード」 が一連で完成です。 例のごとく、まずは“動く芯”を作ってから、モデル切り替え、長文分割、プロンプト改善などに発展させていきましょう!