site-reels について
site-reels は、ローカルで動作するサイトのシーンをスクリプトに沿って録画し、動画素材やショーリールを作るツール群です。サイトや製品の動画を制作するチーム向けに、ブラウザー録画と決定論的なフレーム取得を提供します。
主な機能
- 01スクリプトによるブラウザーシーン録画
- 02サイズを設定できる MP4 出力
- 03snap.mjs による時間制御フレーム取得
- 04シーン準備と操作用フック
- 05Remotion によるショーリール合成ガイド
- 06動画とフレームの確認手順
活用シーン
- 01サイトのブランドシーンの録画
- 02製品操作のショーリール用収録
- 03決定論的な 60fps 素材の作成
- 04Figma Slides 用動画カバーの準備
MCP 対応
README は、スライドのプレースホルダー作成やカバー画像のアップロードに Figma MCP ツール(use_figma、upload_assets など)を使う手順に触れています。MP4 は MCP 経由でアップロードしないと説明しています。
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ pedrojaques99/ visant-skills-public --skill site-reelsインストール後、Claude Code で/site-reelsを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/cases.mdscripts/deliver.example.shfind-chrome.mjsloop.shpatch-timeweb.mjsrecord.mjsscenes.example.mjssnap.mjs詳細
Site Reels:サイト → MP4 → デッキ
要件
| 必要なもの | 内容 | 確認方法 |
|---|---|---|
| すべて | GPU と表示可能なデスクトップ環境がある Windows、macOS、または Linux(Chrome はヘッドフルで実行) | n/a |
| すべて | Node.js 18 以降 | node -v |
| すべて | PATH に ffmpeg と ffprobe があること(または FFMPEG=<full path>) | ffmpeg -version |
| すべて | Chrome、Chromium、または Edge(以下を参照して自動検出) | node -e "import('./scripts/find-chrome.mjs').then(m=>console.log(m.findChrome()))" |
record.mjs | puppeteer-core と puppeteer-screen-recorder(npm、プロジェクト内) | npm ls puppeteer-core puppeteer-screen-recorder |
snap.mjs | puppeteer-core と timesnap(timeweb が含まれます) | npm ls timesnap |
| ショーリール | Remotion プロジェクト | npx remotion --version |
| 監査(任意) | ImageMagick | magick -version |
| Figma へのアップロード | Figma MCP(use_figma、upload_assets)とスキル figma-use、figma-use-slides | MCP ツール一覧 |
Chrome の検出(record.mjs と snap.mjs が使用する scripts/find-chrome.mjs):CHROME
環境変数が設定されていればそれを使用します。未設定の場合は、インストール済みの Chrome/Edge(Windows:Program Files と LocalAppData、macOS:
/Applications と ~/Applications、Linux:PATH 上の google-chrome、chromium、microsoft-edge)を使用します。
それも見つからない場合は、最新の ms-playwright/chromium-<N>(または PLAYWRIGHT_BROWSERS_PATH)を使用します。それも見つからない場合は停止し、
CHROME の設定方法または npx playwright install chromium の実行方法を案内します。Windows では
--use-angle=d3d11 を追加します。それ以外の OS では、Chrome はデフォルトの GPU バックエンドを使用します。
概要
ローカルで実行中のサイトのシーンをスクリプトで録画し、Figma ですぐに使える MP4 を出力します(1920×1080、 30fps、x264、100MB 未満)。検証済みエンジン:puppeteer-core + puppeteer-screen-recorder(CDP のスクリーンキャスト + ffmpeg)。画面録画は使用しません。
実行
mkdir <proj>-reels && cd <proj>-reels && npm init -y && npm i puppeteer-core puppeteer-screen-recorder
mkdir -p scripts && cp ~/.claude/skills/site-reels/scripts/{record,find-chrome}.mjs scripts/
cp ~/.claude/skills/site-reels/scripts/scenes.example.mjs scenes.mjs # edit the scenes
# in the site repo: npx next build && npx next start -p 3100 (production build: smoother WebGL)
OUT="<output folder>/Videos" node scripts/record.mjs scenes.mjs [scene ...]
スクリプトはプロジェクトにコピーしてから実行してください。ESM は cwd ではなく、スクリプトの
フォルダーから puppeteer-core を解決します。
環境変数:BASE(デフォルトは http://localhost:3100)、OUT、CHROME、FFMPEG、W、H、DPR、
MOBILE=1(タッチエミュレーション)、KEEP_UI=1(ナビを表示)、MIN_FPS(24)。スマートフォン向け 4:5:
W=360 H=450 DPR=2 MOBILE=1 で 720x900 になります。
各シーンは { name, path, theme?, trim?, crf?, css?, prepare(page, ctx), act(page, ctx) } です。
prepare は録画前に実行します(セクションまでスクロールする、3D の読み込みを待つなど)。act は
動画に含める操作です。ctx には、イージングに対応した wait、scrollTo(page, y|selector, ms)、
scrollThrough(page, selector|null, ms, from, to) が含まれます。読み込み時に再生するイントロでは、
act 内で page.reload() を使い、読み込み部分をカットするために trim を指定します。
効果的なブランド向けスクリプト:ロゴ/ヒーローのイントロ(ライトとダーク)、ヒーローメディアの切り替え、 スクロールに合わせてカメラが入り込む 3D セクション、イマーシブルなルート(歩行)、フッターのワードマーク、 およびページ全体を通したスクロールを 1 回。 12~55 秒のシーンを 5~7 個用意します。
納品前の監査(終了コードを信用しないでください)
- 動画ごとにコンタクトシートを作成します:
ffmpeg -i x.mp4 -vf "fps=6/<dur>,scale=480:-1,tile=3x2" -frames:v 1 sheet.pngを実行し、目視で確認します。 - 19 秒で 1MB の場合は、シーンが空白です。100MB を超える場合は Figma に入りません(シーンの
crfで修正します。粒子と軌跡があるシーンでは 24 にします)。 - 上端のストリップ(
crop=1920:120:0:0)を複数の時点で確認し、ナビが映り込んでいないかチェックします。 - 冒頭/末尾に長い一時停止がある場合の
LOW-FPSは誤警告です(計測器は静止フレームを数えます)。コンタクトシートを確認してください。
既知の落とし穴
- 非表示ウィンドウで rAF が停止する → フレームがフリーズします。起動時にはすでに
--disable-backgrounding-occluded-windows --disable-renderer-backgrounding --disable-background-timer-throttling --disable-features=CalculateNativeWinOcclusionを指定しています。 意図的にheadless: falseにしています。ヘッドレスモードでは SwiftShader にフォールバックし、WebGL がカクつくためです。 - UI を隠す CSS はリロード時や、React のハイドレーションで
<head>がクリアされたときに消えます → スクリプトは 100ms ごとに<style id="reels-hide">を body に再追加します。 - 無効なセレクターが 1 つあるとルール全体が無効になります:JS のテンプレートリテラル内の
\[は[になり、.z-[60]によってheader, nav, .z-[60]全体が無効になります。危険性のあるセレクターごとにルールを 1 つずつ作成してください。 [data-cursor]は非表示にしないでください:サイトでは 3D キャンバスやカルーセルにもこの属性が付いています。カスタムカーソル要素(fixed + mix-blend + pointer-events-none)を対象にしてください。- 3D は遅延読み込みされます(IntersectionObserver + 動的 import)。セクションに到達してから 5~7 秒待ってください。
preserveDrawingBufferがないと WebGL キャンバスを読み取っても空になります。固定時間待つか、スクリーンショットを使ってください。 - スクリーンキャストで取得できるのは毎秒約 32 個の固有フレームです → 30fps で録画してください(60fps では不規則に同じフレームが繰り返されます)。デフォルトの
trim0.3s では、最初のキャッシュ済みフレームが除外されます。 - 負荷の高いマシンではスクリーンキャストがカクつきます(Photoshop の起動中、並行レンダリング中など)。MP4 の表示は「30fps」でも、毎秒 9~18 個の固有フレームしかありません。
record.mjsは計測(mpdecimate=hi=64:lo=24:frac=0.05)し、MIN_FPS(24)を下回ると最大 2 回まで再録画します。WebGL/3D プレートではsnap.mjsを優先してください。 - Cookie バナー:録画前に
prepareで同意してください(「Accept/Aceitar/OK」を汎用クリック)。 - スクロールしないツールページ(ジェネレーター、エフェクトエディター)は静止画になります。シーンを操作する必要があります(
input[type=file]にファイルをアップロードし、ボタンのテキストを使ってプリセットをクリックします)。カスタムスライダーは合成inputイベントに反応しません。
60fps 固定プレート:snap.mjs(決定論的)
scripts/snap.mjs は timeweb(timesnap の仮想クロック:rAF、performance.now、setTimeout、video)を注入し、フレームごとに 1/60 秒進めてスクリーンショットを撮影します。マシンの負荷に左右されません。
シーン:{ name, path, theme, css, dur, wait (3D 読み込みまでの実時間 ms), pre (スクロール前の仮想 ms、2000。ロード時に再生されるイントロでは **0**), start (スキップする仮想秒数), scroll: { sel, from, to, at, len } | { bottom: [from, to] in screen heights }, frame(page, t) (例:マウス) }。スクロールは仮想時間に応じて行われます。
npm i puppeteer-core timesnap && mkdir -p scripts
cp ~/.claude/skills/site-reels/scripts/{snap,find-chrome,patch-timeweb}.mjs scripts/
npm pkg set scripts.postinstall="node scripts/patch-timeweb.mjs" && npm run postinstall
FPS=60 OUT=... node scripts/snap.mjs scenes-snap.mjs [scene]
timesnap()自体は使わないでください:Next + R3F + Lenis の組み合わせでは、空白のページが録画され、WebGL ヒーローでフリーズした区間が残りました(rAF が動作しません)。独自のループ(goTo + scrollTo + スクリーンショット)はどこでも動作し、60/60 個の固有フレームを取得できました。- timeweb パッチは必須です:Promise も
playingイベントも返さないバージョンにvideo.play()を置き換えます。そのためvideo.play().catch()はエラーになり、ページが空白になります。 - getContext で
preserveDrawingBuffer: trueを強制します(preserve: falseで無効化できます)。 - フレームは OS の一時ディレクトリに保存し、同期ドライブには保存しないでください(数千枚の jpg が同期されます)。
- GPU 処理を並行して実行しないでください(Remotion のレンダリング、Photoshop など)。WebGL のコンテキストが失われ、背景だけが何秒も録画されます。スクリプトは
FLASH(輝度によるフレームのちらつき)を警告します。長い空白区間はコンタクトシートでしか分かりません。確認してください。 - 実際の滑らかさの監査:フル解像度の隣接フレーム間で
magick compare -metric MAEを実行します。0 は同じフレームです。 - 出力:
scripts/deliver.example.sh(プレートをコピーして名前を変更します。V=は必須)。reveal、ループ(loop.sh)、クリーンな 3D を生成し、95MB 未満になるまで crf を 3 ずつ上げます。
Figma Slides にアップロードする(非表示にしてチームの作業を妨げない)
MP4 は MCP 経由では取り込めません。use_figma にはネットワーク機能がなく、Plugin API ではバイト列から動画を作成できません。また、upload_assets が受け付けるのは画像のみ(≤ 10MB)です。そこで、次の手順を実行します。
- 各動画のカバー画像を作成します:
ffmpeg -ss <t> -i x.mp4 -frames:v 1 -q:v 2 _capas/x.jpg。 use_figma(スキルfigma-useとfigma-use-slides)で、最後のスライドの前にcreateSlideRow(idx)を実行し、row.name = 'WIP · Motion (hidden)'とします。動画ごとにisSkippedSlide = trueを設定したcreateSlide(idx, i)を作成し、背景を黒にして、1920×1080 の長方形にVIDEO · x.mp4という名前を付け、speakerNotesに手順を記載します。- 長方形の
nodeIdsを使ってupload_assetsを実行し、カバー画像をマルチパート POST でアップロードします。 - ユーザーが MP4 を長方形にドラッグして、カバー画像を動画に置き換えます。この点を明確に伝えてください。動画を挿入済みだと決して約束しないでください。
getSlideGrid() はスキップされたスライドを省略します。SLIDE_GRID.children[i].children で構造を確認してください。
ブランドのショーリール(Remotion):プレート+デザインしたシーン
ブランドのショーリール(ロゴ、色、書体、レイアウト、マニフェスト、モックアップ)では、サイトからは クリーンなプレート のみを記録します(シーンごとの CSS でタイトル/ナビを非表示にし、テキストを含めません)。それ以外は Remotion で構成します(フレーム単位でレンダリングするため、ちらつきません)。src/fx.tsx に再利用可能な構成要素として Mist、MistWords、Grain、KenBurns、Plate、nevoa()(霧)トランジションを用意し、ブランドデータは brand.ts に記述します。
- ビジュアルアイデンティティはサイトではなくスライドを基準にします。公式の書体とカラーパレットはスライドで確認してください(サイトはブランドブックからずれていることがあります)。
- 効果的なシーン:霧の中に浮かぶオープニングのフレーズ、録画したリビール、ロゴの構築(
evolvePathとロゴのラスターから計測したガイド)、フレーズごとのマニフェスト、可変のwght軸を使った柱、帯状のカラーパレット、タイポグラフィ、重ねたグラフィック、mix-blend-differenceのタイトルを付けた 3D、スクロールするレイアウト列、1/2/3 枚構成のモックアップモンタージュ、名前に同期する製品、ワードマークで締めくくる構成。 - レンダリング前に 静止画(
npx remotion still ... --frame=Nとモンタージュ)で確認します。 --concurrency=3でレンダリングします。Photoshop を開いた状態で 8 に設定すると、32GB のマシンでもメモリが不足しました。- ループ:
scripts/loop.sh in out [start] [dur] [crossfade]。最初と最後のフレームをmagick compare -metric RMSEで比較し、継ぎ目を確認します(< 0.5% なら見えません)。 - 悪いプレートは破棄します。回転する回転面では動きが見えず、縦長キャンバス用に構図を決めたカメラでは 16:9 で小さく見えます。
- フレンティックなリール(承認されたリズム):25~35 秒、60fps、120 BPM のグリッド(30 フレーム)に合わせたハードカットの
Series、3~6 フレームごとの画像バースト、グリッドに重ねる巨大なロゴのクロップ、点滅するフォントウェイト、並べ替わるカラーパレットのブロック、円形マスク内のポートレート、グラフィックのバースト、キネティックなフレーズ、1 拍のモックアップ、約 2.5 秒表示する締めのロゴ。ラベルは一切なし。テキストはグラフィックとしてのみ使います。スローな 1:40 のショーリールは却下されました。 - ソフトで煙のようなリール:ヒーローの煙を ロゴも写真も入れずに 記録し、真っ白な背景に黒で表示します(
storage= localStorage のパラメーター、block: ['assets/logo.png']を指定したシーン)。空白の PNG は透過である必要があります。動画はブロックしないでください。ブロックすると動画が壊れ、timeweb が停止します。プレートはmix-blend-mode: multiplyのオーバーレイとして使います。シーンの冒頭で開き、反転版(ffmpeg -vf reverse)で閉じます。カットは黒で終わります。約 1.1 秒(速度 2)でトランジションすると、シーンの半分が黒くなります。 - Figma Slides のレイアウトを組み立てる:スライド(スキップ済み)を複製し、各カードを 3 回複製して、背景/ロゴ/テキストのみが表示されるようにします。
download_assets(scale 2)で各ノードをダウンロードし、複製を削除します。Slides は 透過をエクスポートしません。ロゴ/テキストをスライドが黒の状態と白の状態でエクスポートし、ImageMagick で差分からアルファを抽出します。use_figma経由で base64 を返さないでください(コンテキストがあふれます)。
製品のショーリール(実際のアプリを撮影)
画面は実際に撮影します(Playwright + CDP のスクリーンキャストを 2x で記録し、手順は
goto/move/click/type/scroll/wait、ポインタートレイルは <id>.cursor.json として保存)。その上に Remotion でカーソルを描き直します。
却下済み。繰り返さないでください:
- デザインしたリールを実際の画面に貼り合わせる(イラストの導入 + 撮影した中盤 + 描画した CTA): 言語が混在し、キャラクターが消えてまた現れ、場面の間に挟まる黒いカードでリズムが途切れます。 1 本の動画につき 1 つの言語にします。
- 16:9 のウィンドウに収めた小さく早回しの製品画面:フィードやランキングが読めない壁になります。
- 画面を切り抜くズームや、動画に重ねて表示するテキスト(大きな動詞、01/07 のカウンター、 グラデーション):邪魔になります。
- 画面上で何もせず待つこと(「waiting…」):待機部分を切り、すぐにアクションを見せます。
- 曖昧なコピー:視聴者が普段使う言葉で、明確に伝わる必要があります。
効果的な形式:アクションごとに、まずテキストだけを中央に短く(2 ビート)表示し、その後、 画面全体を切り抜かず、キャプチャ時のアスペクト比のまま、すっきりした背景の中央ウィンドウに表示します。 控えめなテキスト(ウェイト 700、単語ごとに軽いぼかしをかけて登場)、カウンターなし、グラデーションなし。 トラックのグリッドに合わせてカットします(14 フレーム = 128.57 BPM)。最大 40 秒です。
効果があった手法:
- キャプチャを早回しする場合:
<OffthreadVideo trimBefore={from} playbackRate={rate}>を使い、クリック フレームを/ rateで再計算します(サウンドとカーソルのリングも同様)。 - 別のコンポジションを途中から開始する場合:
<Sequence from={X}><Sequence from={-start}><Comp/></Sequence></Sequence>。 その区間の最初のフレームを確認します。共通要素が「飛び込んで」現れると、位置がずれて見えます。数 フレーム飛ばし、手前にずれてしまったインパクト音を再配置します。 - コンポジションごとの言語設定には、モジュールレベルのタイムラインを使います。Root より前にインポートする各エントリーファイルで、独自の
globalThisフラグを設定します。 - カットをまたいでトラックを途切れさせない場合:ベッドトラックに
trimBeforeを設定し、前の区間が止まった位置から続けます。 - ラウドネス:レンダリング後、2 パスの
loudnorm(-14 LUFS / -1 dBTP)で測定・正規化し、動画はコピーします。測定は最後にだけ行います。Remotion の未処理ミックスは 0 dBFS を超えます。 - 監査:すべてのレンダリングのタイムスタンプ付きコンタクトシート(
fps=2,drawtext=%{pts\:hms},tile=8x6)と、各区間の接続箇所を対象にした高密度シート(fps=6)。 - git がなく、別のセッションでも同時に編集されているプロジェクトの場合:新しいカットは新しいファイルに作成し、共有ファイルを編集する前に
*.bak-*としてバックアップを取り、ユーザーに伝えます。
ポートフォリオカードとスタジオのリール
record.mjsをKEEP_UI=1(カードにサイトの現在の状態を表示)と、上記のスマートフォン用プリセットで実行します。- シーンはインベントリファイルから生成します(公開されていて、有効な URL があるアイテムのみ)。有効な URL がない場合は、スクリーンショットにケン・バーンズ効果を適用します。
- カードのパッケージ:最後から最初へのクロスフェードループ、KB 上限に合わせた 2 パスのビットレート調整、VP9 WebM も併せて出力、フレーム 1 の WebP ポスター。
- Web 用のアルファ付きロゴ:
--codec=vp9 --pixel-format=yuva420p(Safari では再生できません)+ 編集用の ProRes 4444。Config.setVideoImageFormat("png")でアルファを保持します。 - Remotion で切り抜き/ストップモーション風に仕上げる方法:時間を 2 フレーム単位に量子化します(24 fps、2 フレームごとにポーズ)。ポーズごとに SVG フィルターを適用します(縁に feTurbulence + feDisplacementMap、クリーム色の縁取りに feMorphology、くっきりしたオフセットシャドウ)。3 つのシードを循環させて「煮え立つ」ような動きを加え、紙のテクスチャを乗算で重ねます。デザイナーの SVG から id でマスコットのパーツを読み取るため(DOMParser)、コードを変更せずに新しいブランディング版へ差し替えられます。
- CC0 の音楽:Free Music Archive(CC0 アルバム)。テンポと最も伸びのある区間は
librosa.beat.beat_track+ ウィンドウ化 RMS で検出します(Python、任意)。
制作の経緯、参考プロジェクト、マシン固有のヒント:references/cases.md。