Communitygithub.com

podcast-digest

將日語Podcast集數(音頻Podcast或雙人視頻通話錄製)轉換為最長180秒的垂直9:16短片,內嵌日語字幕。

podcast-digest 是什麼?

這是一個專業的AI Agent技能,用於將日語Podcast或視頻錄製(如StreamYard雙人對話)自動生成最多180秒的垂直精華短片。它支援音頻Podcast(input.mp4 + shownote.txt)和雙人視頻錄製(如StreamYard),輸出為1080x1920、30fps的H.264+AAC MP4檔案,內嵌基於ASS字幕的日語字幕,音量標準化至-14 LUFS,各片段間以硬切銜接並附加50ms音頻淡出以避免爆音。技能運作流程為:Step 0環境檢查與目錄建立→Step 1a自動轉錄(whisper-1)→Step 1b人工校正文字(透過corrections.md)→Step 2人工選取精華片段(撰寫clip.draft.json,支援多個連續段落,總長≤180秒)→Step 3自動渲染。人工有兩個檢查點:校正文字後和使用者確認,以及精華方案後獲得批准。技能特別適合處理「幫我剪一段講到XX的部分」、「整理重要重點」等指令,支援關鍵字搜尋與多段落組合。對開發者而言,此技能是基於Python腳本、OpenAI API和ffmpeg/libass的確定性工具鏈,確保結果可重複且易於除錯,適合整合智慧內容剪輯工作流程。

相容平台✓Claude Code✓Codex CLI~Cursor
npx skills add https://github.com/tomoima525/podcast-digest-skill/tree/HEAD

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

Podcast / 錄製 → 精華影片

確定性工作(音頻、API 呼叫、時間計算、渲染)由腳本完成。 你需要執行兩個判斷步驟:校正逐字稿 (1b) 和選擇精華片段 (2)。有兩個人類檢查點。絕對不能跳過。

下面的 SKILL_DIR 指的是包含此檔案的目錄。使用 uv 執行腳本,它會從任何工作目錄使用此技能的獨立鎖定環境: uv run --project "$SKILL_DIR" "$SKILL_DIR/scripts/<name>.py" <episode_dir>。 在以下步驟中,<name>.py <ep> 是該指令的縮寫。每個腳本都會印出它製作了什麼或跳過了什麼。重新執行永遠是安全的:已完成的步驟會被跳過,且當輸入變更時衍生的檔案會重新建置。--force 會重做某個步驟。

集節目錄結構(每集一個目錄)

episodes/<slug>/
  input.mp4                       由使用者提供(音頻 podcast 或影片錄製)
  shownote.txt                    由使用者提供(建議:姓名、產品、主題)
  episode.json                    你為影片錄製撰寫:{"layout": "stacked"}
  audio.mp3, transcript.raw.json  步驟 1a  (transcribe.py)
  corrections.md                  步驟 1b  (你撰寫)
  transcript.json                 步驟 1b  (apply_corrections.py)
  clip.draft.json                 步驟 2   (你撰寫)
  clip.json                       步驟 2   (build_clip.py)
  layout.preview.png              步驟 2   (preview_layout.py, 影片錄製)
  digest.ass, digest.mp4          步驟 3   (render.py)

兩種集節目,同一條管線

音頻 Podcast影片錄製 (StreamYard 雙人畫面)
episode.json無 (或 {"layout": "background"}){"layout": "stacked"}
圖片assets/background.png左側講者方塊在上,右側方塊在下,字幕在中間帶狀區域
聲音audio.mp3從 input.mp4 中裁切,與畫面同步

其他所有部分(轉錄、校正、精華選擇、字幕、音量)都相同。50 分鐘、約 1.5 GB 的錄製也沒問題:只會上傳其音頻,且渲染會直接跳到每個精華片段。

步驟 0:設定

  1. 執行 check_env.py(不需集節目參數)。它會檢查 ffmpeg/libass、背景圖片、字型,以及 OPENAI_API_KEY 是否已在 shell 中設定或存在於 $SKILL_DIR/.env。如果它退出碼為 1,請停止並引導使用者參閱 README → 設定。
    • 絕對不要讀取、列印、cat 或 grep .env 檔案,也絕對不要要求使用者將金鑰貼到對話中。使用者自行將金鑰放入 .env:cp .env.example .env,然後編輯該檔案。
  2. 建立 episodes/<slug>/(簡短的 ASCII slug,例如 ep042)。將錄製檔案複製到其中,命名為 input.mp4,並將節目筆記複製為 shownote.txt。如果沒有節目筆記,請向使用者詢問來賓和關鍵名稱/術語,並自行將其寫入 shownote.txt。 不要移動或複製你未被給予路徑的錄製檔案。
  3. 如果是兩人並排的影片錄製,請撰寫 episode.json:{"layout": "stacked"}。
  4. 執行 run_all.py episodes/<slug>。它會執行步驟 1a 並在步驟 1b 處以退出碼 10 停止。這個暫停是預期的。退出碼 1 表示錯誤:請閱讀錯誤訊息。

步驟 1a:轉錄(腳本)

run_all.py 已執行 transcribe.py。它會提取單聲道 16 kHz 音頻,並使用字詞時間戳、language="ja" 以及從節目筆記建構的詞彙提示來呼叫 whisper-1。長檔案會自動分割成重疊的區塊。whisper-1 有時會跳過一段語音:已辨識字詞之間長時間的停頓但仍有聲音的部分會單獨重新發送,並插入復原的片段(gap ... recovered 行)。若要查看提示詞,請執行 transcribe.py <ep> --print-prompt。若要覆蓋提示詞,請將文字放入 <ep>/prompt.txt 並使用 --force 重新執行。重新執行會花費 API 費用,因此請先詢問使用者。

步驟 1b:校正逐字稿(你)

目標:使用 shownote.txt 作為名稱、產品和術語的正確來源,修正每個片段 text 中辨識錯誤的字詞。

  1. 完整讀取 shownote.txt。列出其中提到的名稱(主持人、來賓)、產品、公司和技術術語。

  2. 讀取逐字稿:show_transcript.py <ep> --raw。對於長集節目,請分段讀取(--from-id/--to-id),並 grep 搜尋每個術語可能聽錯的詞(--grep クロード --grep claude)。Whisper 通常會用片假名寫英文名稱、拼錯(Wisper)、漏掉字母(NTHROPIC),或選錯同音漢字(制度→精度)。

  3. 撰寫 corrections.md:一個 Markdown 表格。每一行取代所列片段中的一個確切子字串:

    | id | before | after | reason |
    |----|--------|-------|--------|
    | 6 | クロットコード | Claude Code | 節目筆記中的產品名稱 |
    | 7, 8, 12 | エーデント | エージェント | エージェント的辨識錯誤 |
    
    • id:片段 ID,以逗號分隔,允許範圍(10-12)。before 必須存在於該片段的當前文字中。行按順序套用。
    • 僅修正辨識錯誤。不要改寫、摘要、移除填充詞或「改進」日文。根據上下文修正明顯的同音詞,並在 reason 中註明「(context)」如果節目筆記無法確定。
    • 如果某個片段是 whisper 的幻覺(例如在靜音中聽到「ご視聴ありがとうございました」),請將其設為空:| 41 | ご視聴ありがとうございました | | hallucination |。
    • 絕對不要合併、分割、重新排序或重新調整片段時間。只要你透過 corrections.md 進行,腳本就能防止這種情況。不要手動編輯 transcript.json。
    • 如果沒有任何需要修正,僅撰寫標題行即可。
  4. 執行 apply_corrections.py <ep>。它會寫出 transcript.json 並執行 validate_transcript.py。如果找不到 before,請修正該行並重新執行。

  5. 檢查點 1。向使用者顯示校正表格(或摘要,加上標記為「(context)」的行),並請他們審查 corrections.md。透過編輯 corrections.md 並重新執行 apply_corrections.py 來套用他們的變更。只有在他們批准後才能繼續。

步驟 2:選擇精華片段(你)

此步驟需要使用者的指示:一個主題(例如「料金の話をしているところ」)或一個請求(例如「大事なポイントをまとめて」)。如果他們尚未提供,請詢問他們。不要猜測主題。

一個短片包含一個或多個片段區間(集節目的連續部分),按時間順序播放,並以硬切銜接。總長:最多 180 秒。

  1. 搜尋:使用指示的關鍵字加上同義詞和可能的拼寫,執行 show_transcript.py <ep> --grep <keyword> ... -C 5。然後,充分閱讀每個命中處前後的內容(--from-id/--to-id),以查看每個討論實際開始和結束的位置。對於「重要重點」,請先分段讀取整個逐字稿,並在選擇前列出候選時刻及其 ID。

  2. 選擇片段區間:

    • 一個片段區間:當指示命名一個主題且其討論適合 180 秒內時。
    • 多個片段區間(通常 2–4 個):當指示要求精華片段,或一個主題分散各處且中間的談話離題時。每個片段區間必須獨自合理,並且緊接在前一個之後也有意義:硬切不提供上下文。
    • 對於每個片段區間:開始於要點被引入的地方,而不是在想法中間或回溯性參考(例如「さっきの」「前回」「それで」)。使用 start_at 修剪前導的填充詞或轉折詞(例如「では本題です。」「で、」)。結束於一個完整的句子;使用 end_after 在片段內停止。
    • 偏好較少但完整的片段區間,而非許多碎片;少於約 8 秒的片段區間很少能獨立存在。
    • 如果素材超過 180 秒,保留最精彩的部分,並在 reason 中說明。
    • 最多新增 2 個 alternatives(其他不錯的片段區間),並附上理由。
    • 如果沒有任何匹配,停止並告訴使用者你搜尋了什麼以及集節目實際涵蓋的內容。不要撰寫草稿。
  3. 撰寫 clip.draft.json。你選擇片段 ID。腳本負責時間計算:

    {
      "instruction": "<使用者的指示,逐字>",
      "title": "スキル機能と文字起こしの要点",
      "reason": "為何選擇這些部分,作為一個整體。",
      "spans": [
        { "start_id": 6, "end_id": 8, "start_at": "今日は、Claude Codeのスキル機能について",
          "title": "スキル機能とは", "reason": "介紹主題。" },
        { "start_id": 12, "end_id": 12, "title": "分担が大事", "reason": "核心重點。" }
      ],
      "alternatives": [
        { "start_id": 120, "end_id": 131, "title": "...", "reason": "..." }
      ]
    }
    

    start_at 和 end_after 是片段區間第一個和最後一個片段文字的可選確切子字串。替代方案也可以使用它們。每個片段區間在多個時都需要一個 reason。(單一片段區間的草稿也可以將 start_id/end_id/... 放在頂層。)

  4. 執行 build_clip.py <ep>。它會將每個邊界對齊到字詞時間戳,增加約 0.3 秒的內邊距(絕不會超過相鄰字詞),排序片段區間,檢查它們是否重疊且總長 ≤ 180 秒,寫出 clip.json,並驗證它。它會列出每個片段區間及其時間範圍和長度。閱讀它印出的任何 warn 行。相鄰的片段區間(接觸的片段 ID)視為重疊:將它們合併為一個片段區間。

  5. 僅限影片錄製:執行 preview_layout.py <ep> 並查看 layout.preview.png(以及 layout.source.png,其中方塊輪廓分別為紅色 = 上方,藍色 = 下方)。每個面板必須顯示一個人,居中,沒有畫面邊緣或相鄰方塊。如果沒有,請在 episode.json 中設定 "tiles"(原始畫面的分數比例)並重新執行:

    {"layout": "stacked",
     "tiles": [{"x": 0.0, "y": 0.14, "w": 0.497, "h": 0.70},
               {"x": 0.504, "y": 0.14, "w": 0.496, "h": 0.70}]}
    

    第一個方塊放在上方。交換兩個項目以交換講者。來自相同錄音室設定的錄製共用相同的方塊,因此可以重複使用有效的 episode.json。

  6. 檢查點 2。向使用者顯示計畫:整體標題和總長度,然後依序顯示每個片段區間及其標題、mm:ss–mm:ss、持續時間、理由,以及它的第一個和最後一個句子(show_transcript.py <ep> --from-id A --to-id B),然後是替代方案。對於影片,也顯示 layout.preview.png。請他們批准、交換、新增或刪除片段區間。

    • build_clip.py <ep> --promote N:單一片段區間時,將替代方案 N 交換進去;多個片段區間時,將替代方案 N 新增為另一個片段區間(注意 180 秒總長)。
    • 任何其他變更:編輯 clip.draft.json 並重新執行 build_clip.py。
    • 只有在他們批准後才能繼續。

步驟 3:渲染(腳本)

執行 run_all.py <ep> --render(或 render.py <ep>)。輸出:digest.mp4,1080x1920,30 fps,H.264 + AAC,音量標準化至 -14 LUFS,並從 digest.ass 內嵌字幕。片段區間以硬切銜接。每個剪輯處會有 50 毫秒的音頻淡出以避免爆音。

然後檢查結果:

  • ffprobe -v error -show_entries format=duration -of csv=p=0 <ep>/digest.mp4 必須與 build_clip.py 印出的總長度相符。
  • 從每個片段區間提取一個影格並查看,以確保字幕有渲染、不是 tofu 方塊,且(影片中)面板取景正確:ffmpeg -y -v error -ss 5 -i <ep>/digest.mp4 -frames:v 1 /tmp/frame.png。
  • 如果渲染警告找不到字型 Noto Sans JP,請告知使用者(README → 設定)。

回報 digest.mp4 的路徑、標題,以及每個片段區間在原始錄製中的時間範圍。

故障排除

症狀修復方式
OPENAI_API_KEY is not set使用者將其添加到 $SKILL_DIR/.env(模板 .env.example)或匯出它。絕對不要自己處理金鑰
before ... not foundbefore 必須是該片段當前文字中的確切子字串(前面的行可能已更改它)
segments changed but not listed每個更改的片段 ID 必須出現在 corrections.md 的一行中
spans total ... > 180s / exceeds 180s刪除一個片段區間,選擇較短的片段區間,或使用 start_at / end_after 修剪
在文字間隙復原功能存在之前製作的逐字稿中缺少語音(人們說話的時間點有長時間跳躍)transcribe.py <ep> --fill-gaps 僅重新發送間隙。它會印出 ID 如何偏移:重新編號 corrections.md / clip.draft.json 以匹配,然後執行 apply_corrections.py <ep>。將任何復原的「ご視聴ありがとうございました」視為幻覺並清空
從同一集製作第二個短片重新命名已完成的 digest.mp4、digest.ass、clip.json、clip.draft.json(例如 digest-keypoints.mp4),然後撰寫一個新的 clip.draft.json
spans overlap兩個片段區間共享或接觸片段:將它們合併為一個片段區間
opens with 'さっき' (warn)稍後開始或使用 start_at 修剪,使片段區間能夠獨立存在
面板顯示錯誤區域 / 邊框修復 episode.json 中的 tiles,使用 preview_layout.py 檢查,然後 render.py <ep>
layout 'stacked' but input.mp4 has no video stream這是音頻 podcast:移除 episode.json
字幕使用備用或細字型assets/fonts/NotoSansJP-Bold.ttf 必須存在(靜態 Bold 檔案,非可變字型);請參閱 README → 設定
更改了 background.png / 字型設定render.py <ep> 會偵測到並重新渲染;可使用 --force 確保

相關技能