Communitygithub.com

podcast-digest

日本のポッドキャスト音声やStreamYardなどの2者ビデオ録画から、縦9:16の180秒以内のショート動画を生成します。発言の要点(「XXの話をしているところ」「大事なポイント」)を人間が指定し、複数のハイライトをハードカットで連結します。文字起こしの修正とハイライト選択の2段階で人間の判断が必要です。

podcast-digest とは?

podcast-digestは、日本のポッドキャスト音声(input.mp4 + shownote.txt)またはStreamYard等で録画された2名のビデオ会話から、最大180秒の縦9:16ショート動画を生成するAIエージェントスキルです。人間が「XXについて話している部分」「重要なポイント」といった指示を与え、該当する箇所を文字起こしから特定・連結して動画をレンダリングします。主なワークフローは、文字起こし(Whisper-1使用)、人間による文字起こしの修正(corrections.md)、ハイライト区間の選択(clip.draft.json)、そしてレンダリングの4ステップです。2つのチェックポイント(修正の確認、ハイライト案の承認)で人間のレビューを挟むことで、品質を担保します。対応するインプットは音声ポッドキャストとビデオ録画の2種類で、`episode.json`の`layout`設定により自動的に処理が切り替わります。スクリプトはPython(uv管理)で実装され、OpenAI API(whisper-1)とffmpegを使用します。このスキルは、Claude CodeやCursorなど、ファイル操作とスクリプト実行が可能なAIエージェント環境で使用することを想定しています。

対応✓Claude Code✓Codex CLI~Cursor
npx skills add https://github.com/tomoima525/podcast-digest-skill/tree/HEAD

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

ポッドキャスト/録画 → ハイライト動画

スクリプトが行うのは確定的な作業(音声処理、API呼び出し、タイミング計算、レンダリング)のみです。 あなたは以下の2つの判断ステップを行います:文字起こしの訂正(1b)とハイライトの選択(2)。2つの人間によるチェックポイントがあります。絶対にスキップしないでください。

SKILL_DIR はこのファイルが置かれているディレクトリを指します。スクリプトはuvで実行します。uvはこのスキル専用のロックされた環境を使用し、任意の作業ディレクトリから実行できます: uv run --project "$SKILL_DIR" "$SKILL_DIR/scripts/<name>.py" <episode_dir>。 以下の手順で、<name>.py <ep> は上記コマンドの省略形です。各スクリプトは作成したものやスキップしたものを出力します。再実行は常に安全です:完了したステップはスキップされ、派生ファイルは入力が変更されたときに再構築されます。--force でステップを強制的にやり直します。

エピソードのレイアウト(1エピソード=1ディレクトリ)

episodes/<slug>/
  input.mp4                        ユーザー提供(音声ポッドキャストまたはビデオ録画)
  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)

2種類のエピソード、1つのパイプライン

音声ポッドキャストビデオ録画(StreamYard 2アップ)
episode.jsonなし(または {"layout": "background"}){"layout": "stacked"}
画像assets/background.png左上に発言者タイル、下に右側タイル、その間の帯に字幕
音声audio.mp3input.mp4 から切り出し、画像に同期

それ以外(文字起こし、訂正、ハイライト選択、字幕、ラウドネス)はすべて同じです。50分、約1.5GBの録画でも問題ありません:アップロードされるのは音声のみで、レンダリングは各ハイライトにシークします。

ステップ0:セットアップ

  1. check_env.py(エピソード引数なし)を実行します。ffmpeg/libass、背景画像、フォント、および OPENAI_API_KEY がシェルまたは $SKILL_DIR/.env に設定されていることを確認します。終了コード1で終了したら、停止してユーザーにREADME→セットアップを参照するよう伝えてください。
    • .env ファイルを読み取ったり、出力したり、cat や grep したりしないでください。また、ユーザーにチャットにキーを貼り付けさせないでください。ユーザー自身が .env にキーを入れます: cp .env.example .env、その後ファイルを編集します。
  2. episodes/<slug>/ を作成します(短いASCIIスラッグ、例:ep042)。録画ファイルを input.mp4 として、ショーノートを shownote.txt としてコピーします。ショーノートがない場合は、ユーザーに出演者と主要な名前/用語を尋ね、自分で shownote.txt に書き込みます。 パスを指定されていない録画ファイルを移動したりコピーしたりしないでください。
  3. 2人の人物が並んでいるビデオ録画の場合は、episode.json を作成します: {"layout": "stacked"}。
  4. run_all.py episodes/<slug> を実行します。これによりステップ1aが実行され、ステップ1bで終了コード10で停止します。この一時停止は想定内です。終了コード1はエラーを意味します:その内容を読んでください。

ステップ1a:文字起こし(スクリプト)

run_all.py がすでに transcribe.py を実行しています。モノラル16kHzの音声を抽出し、whisper-1 を呼び出して、単語レベルのタイムスタンプ、language="ja"、およびショーノートから作成された語彙プロンプトを使用します。長いファイルは自動的に重複するチャンクに分割されます。whisper-1 は時々音声の一部をスキップします:認識された単語の間に長い休止があり、そこにまだ音声が含まれている場合、その部分だけが再送信され、回復されたセグメントが挿入されます(gap ... recovered 行)。プロンプトを確認するには、transcribe.py <ep> --print-prompt を実行します。プロンプトを上書きするには、テキストを <ep>/prompt.txt に置き、--force で再実行します。再実行にはAPI費用がかかるため、最初にユーザーに確認してください。

ステップ1b:文字起こしの訂正(あなた)

目標:各セグメントの text 内の誤認識された単語を修正します。shownote.txt を名前、製品、用語の信頼できる情報源として使用します。

  1. shownote.txt をすべて読みます。そこに含まれる名前(ホスト、ゲスト)、製品、企業、専門用語をリストアップします。

  2. 文字起こしを読みます:show_transcript.py <ep> --raw。長いエピソードの場合は、範囲指定(--from-id/--to-id)で読み、各用語の誤認識されそうな箇所を grep します(--grep クロード --grep claude)。Whisper は通常、英語の名前をカタカナで書き、スペルミス(Wisper)、文字落ち(NTHROPIC)、または誤った漢字の同音異義語(制度→精度)をすることがよくあります。

  3. corrections.md を作成します:1つの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:ハイライトの選択(あなた)

このステップでは人間の指示が必要です:トピック(「料金の話をしているところ」)または最も良い部分のリクエスト(「大事なポイントをまとめて」)のいずれかです。指示がない場合は、それを尋ねてください。トピックを推測しないでください。

ショート動画は1つ以上のスパン(エピソードの連続部分)で構成され、時系列順に再生され、ハードカットで結合されます。合計:最大180秒。

  1. 検索:show_transcript.py <ep> --grep <keyword> ... -C 5 を、指示のキーワードとその同義語、可能性のあるスペルバリエーションで実行します。次に、各ヒットの周辺を余裕を持って読み(--from-id/--to-id)、各議論が実際にどこで始まりどこで終わるかを確認します。「大事なポイント」の場合は、最初に範囲指定で文字起こし全体を読み、IDとともに候補となる箇所をリストアップしてから選択します。

  2. スパンを選択します:

    • 1つのスパン:指示が1つのトピックを指定し、その議論が180秒以内に収まる場合。
    • 複数のスパン(通常2~4つ):指示がハイライトを求めている場合、または1つのトピックが分散していて間の話がトピック外である場合。各スパンは単独でも、前のスパンの直後でも意味が通じる必要があります:ハードカットは文脈を提供しません。
    • すべてのスパンについて:開始はポイントが導入される場所で、思考の途中や後方参照(「さっきの」「前回」「それで」)から始めないでください。先頭のフィラーやトランジション(「では本題です。」「で、」)は 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が接する)は重複としてカウントされます:1つのスパンにマージしてください。

  5. ビデオ録画のみ:preview_layout.py <ep> を実行し、layout.preview.png(および layout.source.png、タイルの輪郭が赤=上、青=下で示される)を確認します。各パネルは1人の人物を表示し、中央に配置され、フレーム端や隣のタイルと重ならないようにする必要があります。そうでない場合は、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}]}
    

    最初のタイルが上に表示されます。2つのエントリを入れ替えると、発言者が入れ替わります。同じスタジオ設定からの録画は同じタイル設定を共有するため、動作する 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:スパンが1つの場合、代替案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 から字幕が焼き付けられます。スパンはハードカットで結合されます。各カットには50msのオーディオフェードが適用され、クリックノイズを防ぎます。

その後、結果を確認します:

  • ffprobe -v error -show_entries format=duration -of csv=p=0 <ep>/digest.mp4 の値が、build_clip.py が表示した合計時間と一致する必要があります。
  • 各スパンからフレームを抽出して確認し、字幕がレンダリングされ、豆腐(トーフ)表示にならず、ビデオパネルが正しくフレームされていることを確認します: 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> を実行します。回復された「ご視聴ありがとうございました」は幻覚として空白にします
同じエピソードから2つ目のショートを作成する場合完成した digest.mp4、digest.ass、clip.json、clip.draft.json の名前を変更し(例:digest-keypoints.mp4)、新しい clip.draft.json を作成します
spans overlap2つのスパンがセグメントを共有または接触している:1つのスパンにマージします
opens with 'さっき'(警告)後から開始するか、start_at でトリミングしてスパンが単独で成り立つようにします
パネルに誤った領域/境界線が表示されるepisode.json の tiles を修正し、preview_layout.py で確認し、render.py <ep> を実行します
layout 'stacked' but input.mp4 has no video streamそれは音声ポッドキャストです:episode.json を削除します
代替フォントや細いフォントで字幕が表示されるassets/fonts/NotoSansJP-Bold.ttf が存在する必要があります(静的Boldファイル、可変フォントではありません);README→セットアップを参照
background.png / フォント設定を変更したrender.py <ep> が検出して再レンダリングします;確実を期すには --force

関連スキル