實戰範例 004:YouTube 影片自動逐字稿提取與多語言 Markdown 筆記生成
簡介與場景
在知識碎片化的時代,許多開發者習慣將 YouTube 上的長篇技術演講或教學影片轉換為文字筆記以便檢索。 本範例將構建一個 AI 筆記生成代理。該代理由兩個主要環節組成:1) 解析並下載 YouTube 逐字稿 (Transcript);2) 利用大型語言模型 (LLM) 摘要內容,並轉換為格式良好的 Markdown 筆記,同時支持多語言翻譯。 這裡面臨的最大技術挑戰是:超長影片的逐字稿往往會超出 LLM 的 Token 限制 (Context Window Limit),且在呼叫外部 API 獲取逐字稿時,常遭遇字幕不存在、版權限制或網路阻擋等邊界條件 (Edge Cases)。
原始代碼:缺乏邊界檢查與過度依賴理想情況
以下代碼假設所有 YouTube 影片都有公開的英文逐字稿,並且將所有文本一次性塞給 LLM。這種做法極易導致 Token 溢出錯誤。
from youtube_transcript_api import YouTubeTranscriptApi
import openai
def generate_video_notes(video_id: str):
# 假設影片一定有字幕,沒有任何錯誤處理
transcript_list = YouTubeTranscriptApi.get_transcript(video_id)
full_text = " ".join([t['text'] for t in transcript_list])
# 直接發送超長文本,極易觸發 Token Limit Exceeded
response = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[
{"role": "system", "content": "You are a helpful assistant. Summarize this in Markdown."},
{"role": "user", "content": full_text}
]
)
with open(f"{video_id}_notes.md", "w", encoding="utf-8") as f:
f.write(response.choices[0].message.content)
generate_video_notes("dQw4w9WgXcQ")
開發者與 AI 的對話記錄
Ponytail (極簡主義者): 「用官方包拿到清單,拼成字串,丟給 OpenAI。這樣寫代碼多乾淨!我討厭那些又臭又長的字串分割演算法。」
Agent Reach (容錯專家): 「乾淨不等於可靠。你這段程式碼有兩個致命傷:第一,遇到沒有字幕的影片會直接拋出 TranscriptsDisabled 異常;第二,一小時的演講逐字稿動輒兩三萬字,直接塞給 GPT 模型會導致 Token 溢出 (Token Limit Exceeded) 或超時。」
Ponytail: 「那我換成支援 128k Token 的模型呢?」
Agent Reach: 「即便上下文夠長,超長輸入也會導致 LLM 的注意力力衰退 (Lost in the middle) 以及極長的等待時間,中途網路斷線的風險很高。我們必須引入『文本分塊 (Text Chunking)』與『降級機制 (Fallback Mechanism)』。如果沒有官方字幕,我們甚至可以優雅地返回提示,而不是讓程序崩潰。」
重構/優化後的代碼:文本分塊與優雅降級
重構後的代碼引入了安全防護,包含:異常捕獲、文本長度驗證、自動分塊摘要,以及完整的重試機制。
import os
import logging
from typing import List, Optional
from youtube_transcript_api import YouTubeTranscriptApi
from youtube_transcript_api.formatters import TextFormatter
from youtube_transcript_api.exceptions import TranscriptsDisabled, NoTranscriptFound
import openai
from tenacity import retry, wait_exponential, stop_after_attempt
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# 配置 OpenAI API Key
openai.api_key = os.getenv("OPENAI_API_KEY")
class VideoNoteGenerator:
def __init__(self, chunk_size: int = 8000):
# 預設每個 Chunk 最大字元數
self.chunk_size = chunk_size
def fetch_transcript(self, video_id: str, languages: List[str] = ['en', 'zh-Hant']) -> Optional[str]:
"""安全地獲取逐字稿,支持多語言降級"""
try:
logger.info(f"正在獲取影片 {video_id} 的逐字稿...")
transcript_list = YouTubeTranscriptApi.list_transcripts(video_id)
# 優先獲取指定的語言,若無則降級為生成的自動字幕
transcript = transcript_list.find_transcript(languages)
formatter = TextFormatter()
text_formatted = formatter.format_transcript(transcript.fetch())
return text_formatted
except TranscriptsDisabled:
logger.error(f"影片 {video_id} 未開放逐字稿功能。")
except NoTranscriptFound:
logger.error(f"找不到指定的語言字幕。")
except Exception as e:
logger.error(f"獲取逐字稿發生未預期錯誤: {e}")
return None
def chunk_text(self, text: str) -> List[str]:
"""將長文本安全地分塊 (Chunking)"""
if not text:
return []
chunks = []
# 簡單的字元分割,實務上可使用 tiktoken 進行精確 Token 分割
for i in range(0, len(text), self.chunk_size):
chunks.append(text[i:i + self.chunk_size])
return chunks
@retry(wait=wait_exponential(multiplier=1, min=4, max=20), stop=stop_after_attempt(3))
def summarize_chunk(self, chunk: str) -> str:
"""帶有容錯機制的 LLM API 呼叫"""
response = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[
{"role": "system", "content": "You are a technical writer. Summarize this transcript chunk into key bullet points in Traditional Chinese Markdown format. Do not add conversational filler."},
{"role": "user", "content": chunk}
],
timeout=30 # 明確設置網路超時
)
return response.choices[0].message.content
def process_video(self, video_id: str, output_file: str):
transcript_text = self.fetch_transcript(video_id)
if not transcript_text:
logger.warning("無法獲取文本,終止筆記生成任務。")
return
chunks = self.chunk_text(transcript_text)
logger.info(f"文本已分割為 {len(chunks)} 個區塊,開始進行摘要...")
final_notes = f"# YouTube Video Notes: {video_id}\n\n"
for idx, chunk in enumerate(chunks, 1):
logger.info(f"正在處理第 {idx}/{len(chunks)} 個區塊...")
try:
summary = self.summarize_chunk(chunk)
final_notes += f"## Part {idx}\n{summary}\n\n"
except Exception as e:
logger.error(f"處理區塊 {idx} 時發生錯誤: {e},跳過此區塊以防全局中斷。")
final_notes += f"## Part {idx}\n*(此區塊摘要失敗)*\n\n"
try:
with open(output_file, "w", encoding="utf-8") as f:
f.write(final_notes)
logger.info(f"筆記已成功保存至 {output_file}")
except IOError as e:
logger.error(f"檔案寫入失敗: {e}")
if __name__ == "__main__":
generator = VideoNoteGenerator()
generator.process_video("dQw4w9WgXcQ", "video_notes.md")
效益分析表格與解讀
| 防護機制 (Protections) | 原始實作缺陷 | 容錯與邊界設計 (Agent Reach) | 對系統穩定性的貢獻 |
|---|---|---|---|
| 字幕獲取容錯 | 依賴靜態 ID,無錯誤處理 | find_transcript 多語言支援與異常捕捉 |
防止腳本因影片權限或無字幕而產生 Crash (崩潰) |
| 超長文本處理 | 一次性傳輸,極易 OOM 或超過 Token 上限 | 導入 chunk_text 分塊演算法 |
控制單次請求的負載,並提高大模型的摘要精確度 |
| 網路不穩定性防禦 | LLM 請求若超時直接掛掉 | 使用 tenacity 模組實現指數退避與重試 |
吸收網路抖動或 OpenAI API 的間歇性 502/503 錯誤 |
| 局部失敗隔離 | 整個流程 All-or-Nothing | 局部捕捉 Chunk 錯誤,容許部分摘要遺失 | 保障了用戶體驗,即使部分處理失敗仍能輸出可用的 Markdown |
解讀:
本範例精準詮釋了如何處理依賴第三方非結構化數據 (YouTube) 與不穩定推理 API (LLM) 的場景。
我們透過 tenacity 處理短暫的網路問題,透過文本分塊處理 Token 邊界限制。最重要的是,在 for 迴圈中我們捕捉了單一 Chunk 的異常,這是一種「優雅降級 (Graceful Degradation)」的展現。這種設計確保了系統「盡力而為 (Best Effort)」,而不是在遇到一點瑕疵時就全盤放棄。