ポッドキャスト/録画 → ハイライト動画
スクリプトが行うのは確定的な作業(音声処理、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.mp3 | input.mp4 から切り出し、画像に同期 |
それ以外(文字起こし、訂正、ハイライト選択、字幕、ラウドネス)はすべて同じです。50分、約1.5GBの録画でも問題ありません:アップロードされるのは音声のみで、レンダリングは各ハイライトにシークします。
ステップ0:セットアップ
check_env.py(エピソード引数なし)を実行します。ffmpeg/libass、背景画像、フォント、およびOPENAI_API_KEYがシェルまたは$SKILL_DIR/.envに設定されていることを確認します。終了コード1で終了したら、停止してユーザーにREADME→セットアップを参照するよう伝えてください。.envファイルを読み取ったり、出力したり、catやgrepしたりしないでください。また、ユーザーにチャットにキーを貼り付けさせないでください。ユーザー自身が.envにキーを入れます:cp .env.example .env、その後ファイルを編集します。
episodes/<slug>/を作成します(短いASCIIスラッグ、例:ep042)。録画ファイルをinput.mp4として、ショーノートをshownote.txtとしてコピーします。ショーノートがない場合は、ユーザーに出演者と主要な名前/用語を尋ね、自分でshownote.txtに書き込みます。 パスを指定されていない録画ファイルを移動したりコピーしたりしないでください。- 2人の人物が並んでいるビデオ録画の場合は、
episode.jsonを作成します:{"layout": "stacked"}。 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 を名前、製品、用語の信頼できる情報源として使用します。
-
shownote.txtをすべて読みます。そこに含まれる名前(ホスト、ゲスト)、製品、企業、専門用語をリストアップします。 -
文字起こしを読みます:
show_transcript.py <ep> --raw。長いエピソードの場合は、範囲指定(--from-id/--to-id)で読み、各用語の誤認識されそうな箇所を grep します(--grep クロード --grep claude)。Whisper は通常、英語の名前をカタカナで書き、スペルミス(Wisper)、文字落ち(NTHROPIC)、または誤った漢字の同音異義語(制度→精度)をすることがよくあります。 -
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を手動で編集しないでください。 - 修正が必要ない場合は、ヘッダ行のみを記述します。
-
apply_corrections.py <ep>を実行します。これによりtranscript.jsonが作成され、validate_transcript.pyが実行されます。beforeが見つからない場合は、その行を修正して再実行します。 -
チェックポイント1。 人間に修正テーブル(またはその要約と、「(context)」とマークされた行)を表示し、
corrections.mdをレビューするよう依頼します。corrections.mdを編集してapply_corrections.pyを再実行することで、人間の変更を適用します。承認を得た後にのみ続行します。
ステップ2:ハイライトの選択(あなた)
このステップでは人間の指示が必要です:トピック(「料金の話をしているところ」)または最も良い部分のリクエスト(「大事なポイントをまとめて」)のいずれかです。指示がない場合は、それを尋ねてください。トピックを推測しないでください。
ショート動画は1つ以上のスパン(エピソードの連続部分)で構成され、時系列順に再生され、ハードカットで結合されます。合計:最大180秒。
-
検索:
show_transcript.py <ep> --grep <keyword> ... -C 5を、指示のキーワードとその同義語、可能性のあるスペルバリエーションで実行します。次に、各ヒットの周辺を余裕を持って読み(--from-id/--to-id)、各議論が実際にどこで始まりどこで終わるかを確認します。「大事なポイント」の場合は、最初に範囲指定で文字起こし全体を読み、IDとともに候補となる箇所をリストアップしてから選択します。 -
スパンを選択します:
- 1つのスパン:指示が1つのトピックを指定し、その議論が180秒以内に収まる場合。
- 複数のスパン(通常2~4つ):指示がハイライトを求めている場合、または1つのトピックが分散していて間の話がトピック外である場合。各スパンは単独でも、前のスパンの直後でも意味が通じる必要があります:ハードカットは文脈を提供しません。
- すべてのスパンについて:開始はポイントが導入される場所で、思考の途中や後方参照(「さっきの」「前回」「それで」)から始めないでください。先頭のフィラーやトランジション(「では本題です。」「で、」)は
start_atでトリミングします。終了は完全な文で;end_afterを使用してセグメント内で停止します。 - 多くの断片よりも、少なく完全なスパンを優先します;約8秒未満のスパンは単独で成り立つことはめったにありません。
- 素材が180秒より長い場合は、最も強い部分を残し、その理由を
reasonに記述します。 - 最大2つの
alternatives(他の有力なスパン)を理由とともに追加します。 - 一致するものがない場合は、停止して人間に検索した内容とエピソードで実際にカバーされている内容を伝えます。ドラフトを作成しないでください。
-
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/... をトップレベルに配置しても構いません。) -
build_clip.py <ep>を実行します。各境界を単語のタイムスタンプにスナップし、約0.3秒のパディングを追加し(隣接する単語を超えない)、スパンを並べ替え、重複や合計180秒超過がないことを確認し、clip.jsonを書き出し、検証します。各スパンを時間範囲と長さとともにリストします。出力されるwarn行があれば読んでください。隣接するスパン(セグメントIDが接する)は重複としてカウントされます:1つのスパンにマージしてください。 -
ビデオ録画のみ:
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を再利用してください。 -
チェックポイント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 found | before はそのセグメントの現在のテキストの正確な部分文字列でなければなりません(以前の行で変更されている可能性があります) |
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 overlap | 2つのスパンがセグメントを共有または接触している: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 |