Communitygithub.com

deck-to-video

スライドデッキとスライドごとのナレーション音声を1つのmp4動画に変換します。自動再生・自動プレゼンテーション形式の動画、「スライドを動画に」「プレゼンテーションをmp4に」、WhatsApp/Slack/Driveで共有可能なナレーション付きデッキ動画、スライド動画にスライドごとのプログレスバー/カウントダウン/スライドカウンターが必要な場合、またはナレーションと画面上のアニメーションの同期が必要な場合に使用します。また、ffmpegで描画したプログレスバーが最初のフレームから完全に表示される、ffmpegエラー「No such filter: 'drawtext'」が発生する場合、デッキ動画に字幕を焼き付けたりハードコーディングする場合、SlackやTeamsのインラインプレイヤーで字幕が表示されない場合、VLCで2組の字幕が同時に表示される場合、ffmpegが字幕フィルターで「No option name near」と報告する場合、最終的なffmpegのmuxで動画の末尾がカットされる場合、またはffmpeg 9が-filter_complex_criptを拒否する場合にも使用します。

deck-to-video とは?

deck-to-videoは、N枚のスライド画像(またはPDF)とN個のスライドごとの音声クリップを1つのmp4動画に合成するスキルです。各スライドはナレーションの長さに短いポーズを加えた時間だけ表示され、次のスライドにハードカットします。オーバーレイとして、経過に合わせて徐々に埋まるプログレスバー、残り秒数のカウントダウン(右下)、「N / M」形式のスライドカウンター(右上)をPILで描画し、ffmpegの画像シーケンスとしてエンコードします。これにより、ffmpegの`drawbox`や`drawtext`で発生する可能性のあるバグ(プログレスバーが動かない、`drawtext`フィルターがないなど)を回避します。また、`--highlights`オプションを使用すると、ナレーションのタイミングに合わせてスライド上の要素をハイライト/グロー表示できます。これはElevenLabsの強制アライメント機能を使って実際の発話タイミングを取得し、それに基づいてアニメーションを生成することで実現します。さらに、アライメント結果から自動的にSRT字幕を生成し、サイドカー、ソフトトラック、または動画に焼き付ける形で出力できます。このスキルはClaude Codeや他のAIエージェントが、プレゼンテーション動画の作成を自動化するために使用できます。

対応~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/shaharsha/claude-skills/tree/HEAD/skills/deck-to-video

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

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

ドキュメント

デッキから動画へ(スライド + ナレーション → 自動再生mp4)

概要

N枚のスライド画像(またはPDF)とN個のスライドごとの音声クリップを1つのmp4に変換します。各スライドは、そのナレーションの長さに短いポーズを加えた時間だけ表示され、その後次のスライドにハードカットします。オーバーレイ:埋まっていくプログレスバー+残り秒数のカウントダウン(右下)と「N / M」形式のスライドカウンター(右上)。

1つのコマンド(スクリプトはこのスキルのscripts/ディレクトリにあります。フルパスで呼び出してください):

python3 <このスキルのディレクトリ>/scripts/build_video.py SLIDES AUDIO_DIR out.mp4
# SLIDES = slides.pdf(200dpiでラスタライズ)またはスライドごとの画像ディレクトリ
#          (数字を考慮した順序付け:s2はs10の前。ゼロ埋めの名前を推奨)
# AUDIO_DIR = slide01.mp3, slide02.mp3, … — 数はスライド数と一致する必要があります
# オプション: --size 1920x1080 · --pad 0.7 · --no-bar · --no-counter · --bar-color HEX · --crf 20
#           --highlights hl.json · --anim-fps 30   (「要素のハイライト」を参照;
#           scripts/make_highlights.py が hl.json を生成します)

成功すると、スライドごとに1行(そのスライドの長さ。すでにポーズを含んでいます)が出力され、最後に duration matches segment sum と表示されます(そうでなければゼロ以外の終了コードで終了します)。

入力の準備

  1. スライド:pptxから始める場合は、LibreOfficeではなく実際のPowerPointからラスタライズしてください。 pptxを実際のPowerPointでPDFにエクスポートし(office-renderスキルがmacOSでこれを行います)、そのPDFをスクリプトに渡します。LibreOfficeはフォントを置き換え、レイアウトを再フローするため、動画がデッキ作成者の見ているものと一致しなくなります。200 dpiで、1080pでも密度の高いスライドテキストを鮮明に保てます。(すでにスライドごとの画像がある場合は、それらを直接使用し、この手順はスキップしてください。)
  2. 音声:スライドごとに1つのクリップを slideNN.mp3 という名前で用意します。 クリップが elevenlabs-tts の generate_tts.py(narrating-pptx が使用)から来ている場合、それらはすでにこの正確な形式になっています。それを再利用することで、pptxと動画のサウンドが同一になります。
  3. ナレーションの速度が調整されている場合(例:ffmpeg -filter:a atempo=1.1)、調整済みのファイルから動画を構築します。

オーバーレイがffmpegではなくPILでベイクされる理由(血の代償)

  • drawbox で t ベースの幅式を使用すると、いくつかのffmpegビルドでフレーム0から完全なバーが描画されます — 式はフレームごとではなく、初期化時に一度だけ評価されます。この失敗は沈黙しています。動画は正常にエンコードされ、バーがまったく動かないだけです。同じビルドでは drawtext 式はフレームごとに更新されるため、動作するカウントダウンの隣で壊れたバーを見逃しやすくなります。
  • Homebrewのffmpegボトル(2026年以降)はlibfreetypeなしでビルドされています — No such filter: 'drawtext' — そしてkegバージョン(ffmpeg@7、ffmpeg@8)も同様です。インストールサイクルを費やしてこれを発見しないでください。もし他の理由でフルビルドが必要な場合、uv tool install static-ffmpeg が1つを提供します(スクリプトは自動的にフォールバックします)。

そのため、スクリプトはPILでフレームをベイク(バーの塗りつぶし、カウントダウン、カウンター)し、-r 30 でエンコードされた画像シーケンスをffmpegに供給します。決定論的、フォント制御可能、そして30fpsのストリームをフィルタリングするよりもエンコードコストが低くなります。1秒あたり1フレームがデフォルトで、スライド上で何も動いていない限りは適切です — 秒単位の更新はカウントダウンにとって自然に読めます。--highlights を伴うスライドは、次のセクションの理由により、代わりに30fpsでベイクされます。

スクリプトがすでに行うエンコードの選択(元に戻さないでください)

選択理由
apad=pad_dur=PAD + -t seg_dur正確なテールパディング。ナイーブな -shortest は発話後のポーズをカットします
yuv420p + 偶数寸法そうしないとQuickTime/WhatsApp/ブラウザで再生できません
-tune stillimage, CRF 20静的スライドはテキストのチラつきなくうまく圧縮されます
concat demuxer + -c copy同一エンコードされたセグメントのロスレス結合
-movflags +faststartダウンロードが完了する前に動画がストリーミングされます

検証 — フレームが証明するまでオーバーレイは壊れていると想定する

スクリプトは最終的な長さ ≈ セグメントの合計をアサートします。オーバーレイは目視での確認が必要です:

ffmpeg -y -ss T1 -i out.mp4 -frames:v 1 a.jpg   # あるスライドの早い時間
ffmpeg -y -ss T2 -i out.mp4 -frames:v 1 b.jpg   # 同じスライドの遅い時間

スクリプトが出力したスライドごとの長さからT1/T2を選択します(スライド境界を累積合計します。これらにはポーズが含まれていることに注意してください)。両方の画像を確認します。b.jpg ではバーが目に見えて埋まっており、カウントダウンが低くなっている必要があります。そして、新しいスライドごとにリセットされる必要があります。早いタイムスタンプでバーが完全に見える = 静的バグ — この正確なチェックがそもそもそれを捕らえたものです。音声をヘッドレスで検証することはできません。その旨を伝え、聞いたと主張しないでください。

ナレーションに合わせて要素をハイライトする

「左側に…中央に…右側に」と言うナレーションは、音声解説トラックのように読めます(セルフプレゼンテーションデッキを参照)。修正方法は、口頭でのポインターを削除し、代わりに要素をグローさせることです。ただし、タイミングが音声から得られる場合に限ります。

音声がマスターであり、アニメーションはそれに従います。 マークに合わせてナレーションを書いたり、フレーズがいつ発話されるかを推定したりしないでください。通常どおりクリップを生成し、ElevenLabsの強制アライメント(POST /v1/forced-alignment、音声+トランスクリプト → 文字ごとの start/end。elevenlabs-ttsの scripts/align_narration.py で実行)を使用して、各要素が実際に発話されたタイミングを読み取り、そこからウィンドウを導出します。後でクリップを再録音し、再アライメントすると自動的に修正されます。

  • スクリプト内の位置にアンカーし、キーワードにはアンカーしないでください。 単一のキーワードは、それが2回出現するとすぐに壊れます(「タグ」は1つのスライドに4回出現します)。ナレーションが各要素に移る場所を、スクリプトからコピーした短いフレーズで、話された順序でマークし、前のマッチの後でのみ各フレーズを検索します。これが make_highlights.py の行うことです。スクリプトの前半に繰り返される単語があると、それをキャプチャできません。(結合されたスクリプトへの要素ごとのセグメントオフセットを保持することも機能します。)
  • アライメント前に音声タグをすべて削除してください — すべて。 [confident] は実行され、発話されません。これをアライナーに送信すると、音声内の単語を探し、後続のすべてのオフセットを引きずります。Eleven v4タグは自由形式([Pause, dry amusement])なので、括弧内のものをすべて削除します(elevenlabs-ttsのアライナーが行うように)。同じ削除された文字列に対してオフセットを計算します。
  • 返された文字数がテキスト長と等しいことを確認し、loss を監視します(正常な英語17クリップデッキでは0.04〜0.10、正常なヘブライ語26クリップデッキでは0.59〜0.77。デッキ内の明確な外れ値がシグナルであり、絶対的なしきい値ではありません)。不一致は、そのスライドのすべてのウィンドウが間違っていることを意味します。

scripts/make_highlights.py を使用して --highlights JSONを構築します。3つの入力から:マーカー(スライドごと、ナレーションが各領域に移るフレーズを含む各領域)、ボックス(スライドごと、領域 → [x, y, w, h])、およびalignment/ファイル:

python3 <このスキルのディレクトリ>/scripts/make_highlights.py markers.json boxes.json narration/alignment/ hl.json \
  --size 1920x1080 [--inches 13.333]      # --inches: ボックスはスライドインチ単位、この幅のページ
# markers.json: {"09": [["chart", "Last quarter"], ["callout", "The real problem"], ["_end", "Now, to"]],
#               "12": {"marks": [["a", "First"], ["b", "Second"]], "dim": "black", "colour": "#E07A5F"}}

領域のウィンドウは、そのフレーズから次のマーカーのフレーズまで実行されます(_end は別のウィンドウを開かずに閉じます。そうでなければ、最後のウィンドウは発話の終わりまで実行されます)。開始は0.12秒早くなります。1.4秒未満のウィンドウは警告とともにスキップされます。0.5秒のフェードインとフェードアウトでは何も表示されないからです。フレーズがスクリプトにない場合、領域にボックスがない場合、ボックスがフレームからはみ出る場合、またはアライメントファイルがスクリプトと一致しない場合、スクリプトは何も書き込まずに停止します。ボックスが端から19ピクセル以内にある場合、グローがクリップされる可能性があるため警告します。

結果のJSONは、ゼロ埋めされたスライド番号をキーとし、ボックスは出力ピクセル単位です:

{"09": [{"card": "chart", "box": [87, 468, 549, 374], "colour": "#B45309",
         "dim": "white", "start": 13.6, "end": 22.9}]}

box はデッキの独自のレイアウトコード(インチ→ピクセル)から取得し、レンダリングを目視で確認して取得しないでください。カードの形状は、それを描いたビルダーですでに正確です。dim は明るいスライドでは white、暗いスライドでは black です。暗いスライドでもグローの色を確認してください(ダークプラムのグローが、ほぼ黒のクロージングスライドで消えて見えなくなりました)。領域は1つのスライドで複数回ハイライトされる場合があります。ファイルに存在しないスライドは、通常どおり1fpsでベイクされます。

アライメントされれば、字幕は自動的に得られます。 同じ単語のタイミングで、2回目の文字起こしパスなしで .srt が作成されます:

python3 <このスキルのディレクトリ>/scripts/make_srt.py narration/alignment/ narration/audio/ out.srt --pad 0.7

--pad は動画が構築されたパッドと一致する必要があります。一致しない場合、キューのタイミングがスライドごとにさらにずれます。

字幕を提供する3つの方法。宛先がどれを選択するかを決定します。

形式方法視聴者が得るもの
サイドカー .srtmp4の隣にファイルを配置本物のプレイヤーで両方を開いた場合のみ字幕表示
ソフトトラック-c copy -c:s mov_text -disposition:s:0 default — ロスレスmux、約12KB、数秒プレイヤー依存。CCコントロールが存在する必要がある
焼き付けlibassで再エンコード常に表示、どこでも、オフにできない

デフォルトはサイドカーまたはソフトトラック — どちらも可逆的で、画像に影響を与えません。字幕コントロールがない場所で動画が再生される場合のみ焼き付けます。 SlackやTeamsスレッドのインラインプレイヤーがそれを強制するケースです。そこですぐに再生ボタンを押した視聴者は、他の2つの形式からは何も得られません。

字幕の焼き付け

ffmpeg -i deck.srt /tmp/subs.ass          # 最初に変換。force_style を渡さないでください
# … /tmp/subs.ass の Style: 行を編集(下記参照)。その後:
<static-ffmpeg> -i master.mp4 -vf "ass=/tmp/subs.ass" \
  -c:v libx264 -crf 20 -preset medium -tune stillimage -pix_fmt yuv420p \
  -c:a copy -sn -movflags +faststart out.mp4

それぞれがサイクルを消費する4つのこと:

  • Homebrewのffmpegにはlibassがありません — drawtext/libfreetypeと同じギャップで、その兆候は誤解を招きます。No option name near '<filename>' という、フィルターグラフ解析エラー(No such filter ではない)が発生するため、引用バグのように見え、すでに正しかったエスケープの書き直しに時間を費やすことになります。ffmpeg -filters | grep -E ' (ass|subtitles) ' で確認し、static-ffmpegビルドを使用してください。
  • .ass ファイルにスタイルを設定します。force_style をフィルター文字列に決して詰め込まないでください。 そのカンマはフィルターセパレーターとして読み取られ、バックスラッシュエスケープは、機能する前に2回失敗するほど脆弱です。
  • ASSスタイルフィールドは、Format: ヘッダーでマッピングし、決して位置を数えてマッピングしないでください。 1つずれると、Alignment=0(無効)が書き込まれ、MarginVが設定されず、libassは文句を言わずにレンダリングします。
  • 焼き付け前のマスターから焼き付け、そのマスターを保持してください。 すでに焼き付けられたファイルからスタイルを変更すると、2世代目の劣化が重なり、かつ古いテキストが永久に焼き付けられます。

配置 — 帯またはオーバーレイ。デッキが決定します。好みではありません。

方法コスト
スライド下の帯pad=1920:1200:0:0:black 次に ass=、帯内のMarginVスライド上の何も覆われない。出力が16:9でなくなる
オーバーレイ(おなじみのVLCルック)ass= のみ。下中央揃え、1080pで太字約48px、太い黒の輪郭、MarginV〜40クラシックで自己完結型。ただし、デッキが下部ストリップに配置するものは何でも覆う

オーバーレイは、ほとんどの人が「字幕」と言うときに意味するものです。テキスト密度の高いデッキでは、コミットする前にフレームを抽出して確認してください。フッターのキャプション、凡例、プログレスバーの上に配置されます。

1fpsはスムーズさだけでなく、同期の限界でもあります。 デフォルトのベイクは1秒あたり1フレームであるため、ハイライトは整数秒でのみ変更できます。指示する単語から最大1秒ずれる可能性があり、壊れているように見えます。アニメーション化されたスライドには約30fpsが必要です。

30fpsは、フレームをキャッシュすれば低コストです。 60秒のスライドは1,800フレームですが、そのほぼすべてがピクセル単位で同一です。グローはフェード中にのみ移動します。各フレームをその視覚状態にキー付けし、重複をハードリンクします:

key = (active_element, round(fade_alpha, 2), int(t))   # int(t) = バーのステップ
if key in cache: os.link(cache[key], path); continue

スライドあたり約1,800フレームではなく、120〜200の実際の合成処理で済み、余分なディスクも必要ありません。ただし、最初にプログレスバーを整数秒に量子化します — 継続的に動くバーはすべてのフレームを一意にし、キャッシュを完全に無効にします。(1fpsではすでにそのように動作していたため、何も失われません。)

暗いスライドでは「dim」を反転します。 非アクティブな要素を半透明の白で洗い流すと、明るいスライドではそれらを後退させ、暗いスライドでは明るくします。dimカラーはデッキごとではなく、スライドごとに指定します。

速すぎる発話はアニメーション化しないでください。 コミットする前に発話時間を測定します。4つのリスト項目がそれぞれ約1.2秒でまくしたてられている場合、0.5秒のフェードインとフェードアウトの後、グローは約0.2秒しか残らず、ストロボ効果になります。各項目にナレーション内で独自のビートを与えるか(通常は改善。一息で言うリストはあらゆるデッキで最も密度の高いものです)、スライドを静的なままにします。

よくある間違い

間違い結果修正
ffmpeg drawbox を t でアニメーション化する静的バー。バーは決して埋まらないPILベイクを使用する。drawtext でも可能だがバーは不可
スライドごとに別々のffmpeg呼び出しを使用する各パス間の不整合、結合時の再エンコード、不正確な末尾スクリプトに任せる
非yuv420pピクセルフォーマット多くのプレイヤーで再生されない明示的に yuv420p を強制
-shortest で終了する(apadの代わり)発話後のポーズがカットされるapad=pad_dur=PAD + -t seg_dur を使用
drawtext でフォントパスを推測するフォントが見つからない、バグのあるエンコードPILがシステムフォントを管理する
同期に -itsoffset を使用するストリーム間でffmpegの内部タイムスタンプが競合するリスクベイクの前に音声ファイルをトリミングする

トラブルシューティング

症状原因と修正
duration mismatch(ゼロ以外の終了コード、スクリプトの最後で報告)セグメントの合計が最終的な長さと一致しません。どのセグメントが原因かをデバッグするには、ffprobe を各ファイルで個別に実行します。ファイルが切り詰められていないか確認します。AUDIO_DIR にスライドよりも多くのファイルがないか確認します。
音声が途切れる、またはクリックノイズが入る元の音声ファイルが破損している、またはサンプルレートが一貫していません。ffmpeg -i で各ファイルを確認します。必要に応じて、ffmpeg -i input.wav -ar 44100 -ac 1 output.mp3 で再サンプリングします。
字幕が表示されない(サイドカーまたはソフトトラック)プレイヤーが字幕をサポートし、有効にする必要があります。VLCで確認します。SlackやTeamsのインラインプレイヤーはサイドカーもソフトトラックもサポートしません。焼き付けが必要な場合です。
字幕がずれている--pad が動画の構築時に使用されたものと一致していません。正しいパッドで .srt を再生成します。
ffmpeg: command not foundffmpegがインストールされていないか、PATHにありません。python3 <scripts>/build_video.py を再度実行するか、uv tool install static-ffmpeg を試してください。
動画が再生されない、またはコーデックエラーが表示される-pix_fmt yuv420p が欠落しているか、寸法が奇数です。すべての出力が偶数であることを確認してください。

Individual skills in this repo

This repo contains 3 individual skills — each has its own dedicated page.

shaharsha/generating-video-clips

Use when turning a still image, illustration, comic panel or character art into a short animated clip (image-to-video), choosing a video model or provider (Gemini Omni, Veo, Seedance, Kling, ElevenLabs Flows) or hitting their API gates, lip-syncing a character's mouth to existing dialogue audio (Sync, fal.ai, active speaker), or fixing a generated clip with artifacts - duplicated props, a door or a second figure appearing, hands through bodies, glowing eyes, a gesture repeated at the start,...

shaharsha/image-generation

Generate logos, icons, UI mockups, hero images, product shots, and other design assets using OpenAI GPT Image 2.5 (Sunburst, Flare) or Google Gemini (Nano Banana 2, Nano Banana Pro, Nano Banana 2 Lite). Use whenever the user asks to create, design, generate, mock up, render, or illustrate a visual asset, needs a transparent PNG, or asks for image prompt engineering. Picks the right model and quality for the job, writes a model-specific prompt following the official prompting rules of each...

shaharsha/producing-animated-episodes

Use when making a narrated or voiced animated video with recurring characters - an anime, cartoon or comic episode, a short for a group chat, a story video about friends, family, a class or a team - from photos, a chat export, a story idea or a script, in any visual style, Hebrew/RTL or English. Also use when such a video has subtitles or cards too fast to read, music stopping early, the ending cut off, lines buried under music, mouths out of sync, a gesture before its line, or one face...

関連スキル