Install any skill in seconds. Free to start, no credit card required.
Get Started Free →参考動画からプロ品質の Remotion 動画を再現するワークフロースキル。 「参考動画から再現したい」「動画トレース」「PVを作りたい」「Remotion動画を参考動画ベースで作りたい」で発動。
.claude/skills/minicoohei-remotion-trace/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-03 | ✗→✓ | ▲ Improved | 206% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 144% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 211% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 135% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 223% | 0% |
参考動画からプロ品質の Remotion 動画を再現するためのワークフロースキル。 企業PV制作の実プロジェクト(16イテレーション)で確立した手法を体系化。
「Remotion動画を作りたい」「PVを作りたい」「参考動画から再現」「動画トレース」
mv-composer/ プロジェクト or 新規)目的に応じてソースを使い分ける:
| ソース | URL | 用途 | |--------|-----|------| | Vimeo Staff Picks | vimeo.com/channels/staffpicks | トランジション・カラーグレーディング | | Art of the Title | artofthetitle.com | タイトルシーケンス・モーショングラフィックス | | Stash Media | stashmedia.tv | CM・プロモ映像のトレンド | | YouTube | 同業種・競合検索 | 企業PV・紹介動画の業界水準把握 | | ゲームトレーラー | Brikk等 | ダイナミックなカット・エフェクト |
「このシーンの表現が欲しい」単位で 5-10秒のクリップ を収集する。動画全体ではなく、特定の演出・トランジションにフォーカス。
bash# YouTube/Vimeo からクリップ単位でダウンロード yt-dlp --download-sections "*57-59" -o "data/video_refs/{project}/{id}_%(section_start)s-%(section_end)s.%(ext)s" "https://youtube.com/watch?v={id}" # Vimeo の場合 yt-dlp --download-sections "*20-30" -o "data/video_refs/{project}/vimeo_{id}_0020-0030.%(ext)s" "https://vimeo.com/{id}"
data/video_refs/{project_name}/
├── {videoId}_{startSec}-{endSec}.mp4 # 参考クリップ
├── frames/ # 抽出フレーム(Part 2で生成)
└── README.md # 各クリップの出典・用途メモ必ず README.md に出典を記録:
markdown## {videoId}_{start}-{end}.mp4 - 出典: {会社名} 公式PV (YouTube) - 用途: 人物紹介のワイプ演出(暗背景→矩形飛散→写真露出)
bash# 参考動画からフレーム抽出(6-10fps推奨) ffmpeg -i data/video_refs/{project}/clip.mp4 \ -vf "fps=8" \ data/video_refs/{project}/frames/clip_%04d.png # 特定区間のみ抽出する場合 ffmpeg -i clip.mp4 -ss 2.0 -t 3.0 -vf "fps=10" frames/%04d.png
抽出フレームを Read ツールで開き、以下の観点で分析する:
各クリップについて以下を記述:
## clip: {videoId}_{start}-{end}.mp4
### アニメーション分解
- 0.0s: 暗背景 + 白い矩形がランダム配置で飛散
- 0.5s: clipPath inset で左→右に写真がワイプイン
- 1.5s: 写真フル表示 + 左下に名前テロップ(白文字 shadow付き)
- 3.0s: 3ストリップに分割、各ストリップが別アングル
### Remotion実装メモ
- clipPath: `inset(0 ${100 - progress}% 0 0)` で実現可能
- 矩形飛散: position:absolute + ランダム top/left/rotationdraw.io等で作成した図解PNGを、Remotion上のReact SVGアニメーションに変換する手順。
| 観点 | PNG(そのまま) | React SVG | |------|----------------|-----------| | アニメーション | 不可(静止画) | 要素が spring() で順番に登場 | | 解像度 | 拡大でぼやける | SVGはどんなサイズでもシャープ | | 微調整 | draw.ioで再編集→再エクスポート | px値やタイミングをコードで即座に制御 | | 2フェーズ遷移 | 不可 | 1シーンで前半/後半で異なる情報を表示可能 |
typescript // ノード → rect + text <rect x={node.x} y={node.y} width={node.w} height={node.h} rx={8} fill={node.color} opacity={nodeOpacity} /> <text x={node.x + node.w/2} y={node.y + node.h/2} textAnchor="middle" dominantBaseline="central" fill="#FFF" fontSize={16}>{node.label}</text>
// 矢印 → path or line <line x1={arrow.x1} y1={arrow.y1} x2={arrow.x2} y2={arrow.y2} stroke="#666" strokeWidth={2} markerEnd="url(#arrowhead)" />
spring() で要素を順番に登場させるtypescript const nodeOpacity = spring({ frame: frame - index * STAGGER_DELAY, fps, config: { damping: 14, mass: 0.6, stiffness: 160 }, }); const nodeScale = spring({ frame: frame - index * STAGGER_DELAY, fps, config: { damping: 14, mass: 0.6, stiffness: 160 }, }); // style: { opacity: nodeOpacity, transform: scale(${nodeScale}) }
typescript const NODES = { input: { x: 100, y: 200, w: 180, h: 60, color: '#3B82F6', label: 'Input' }, process: { x: 400, y: 200, w: 180, h: 60, color: '#10B981', label: 'Process' }, output: { x: 700, y: 200, w: 180, h: 60, color: '#F59E0B', label: 'Output' }, } as const;
静的PNGが <Img> で3秒以上表示されている場合 → 「アニメーション化を検討」フラグを出す。 motion-review スキルの J3 チェック項目と連動。
bash# 参考動画から音声抽出 ffmpeg -i reference.mp4 -vn -acodec pcm_s16le ref_audio.wav # 手動カウント: 10秒間のbeat数を数えて × 6 # または ffmpeg のエネルギー検出で波形を可視化 ffmpeg -i ref_audio.wav -af "showinfo" -f null - 2>&1 | head -50
BPM = 103 の場合:
1 beat = 60/103 = 0.5825s
5 beats = 2.91s
9 beats = 5.24ssectionDurations は必ず beat の整数倍にする。これにより BGM の拍とシーン切り替えが自然に同期する。
Stable Audio 2.5(推奨): 最大190秒を一発生成可能。ジョイント問題なし。
javascript// fal.ai Stable Audio 2.5(最大190秒) const result = await fal.subscribe("fal-ai/stable-audio-25/text-to-audio", { input: { prompt: "...", seconds_total: 80 }, }); const url = result.data?.audio?.url;
bash# 末尾フェードアウト(4秒)を適用して mp3 に変換 ffmpeg -y -i raw.wav -af "afade=t=out:st=76:d=4" -q:a 2 bgm_final.mp3
代替モデル(fal.ai): | モデル | API ID | 最大尺 | 用途 | |--------|--------|--------|------| | Stable Audio 2.5 | fal-ai/stable-audio-25/text-to-audio | 190秒 | 汎用(推奨) | | Beatoven maestro | beatoven/music-generation | 150秒 | ライセンス済み商用BGM | | CassetteAI | cassetteai/music-generator | 180秒 | 低コスト高速 | | Stable Audio (旧) | fal-ai/stable-audio | 47秒 | 非推奨(分割+crossfadeが必要) |
旧方式(47秒制限の場合のみ): 複数パートを crossfade で連結
bashffmpeg -i part1.mp3 -i part2.mp3 \ -filter_complex "[0][1]acrossfade=d=2:c1=tri:c2=tri" \ -y bgm_full.mp3
TaxAccountantDemo v34-v40 で確立したワークフロー。
bash# ElevenLabs multilingual v2 + 日本語向け推奨設定 VOICE_ID="StTDrGrPSyfaHGmzwXbj" # Masa(落ち着いた日本語男性) SETTINGS='{"stability":0.70,"similarity_boost":0.80,"style":0.10,"use_speaker_boost":true}' curl -s -X POST "https://api.elevenlabs.io/v1/text-to-speech/${VOICE_ID}" \ -H "xi-api-key: ${ELEVENLABS_API_KEY}" \ -H "Content-Type: application/json" \ -d '{"text":"テキスト","model_id":"eleven_multilingual_v2","voice_settings":'"${SETTINGS}"'}' \ --output "narration.mp3"
ElevenLabs multilingual v2 は日本語の漢字を中国語読みすることがある。
対策: 問題が起きやすい漢字をひらがなに置換してから生成
税理士 → ぜいりし 記帳 → きちょう 仕訳 → しわけ
取引 → とりひき 損益 → そんえき 即時 → そくじ
即座 → そくざ 監視 → かんし 瞬時 → しゅんじ
24時間365日 → にじゅうよじかん さんびゃくろくじゅうごにち
12,800円 → いちまん にせん はっぴゃく えんstability を 0.55→0.70 に上げると中国語混入が減る。
pythonimport google.generativeai as genai model = genai.GenerativeModel("gemini-2.0-flash") audio = genai.upload_file("narration.mp3") resp = model.generate_content([ audio, "この日本語音声を正確に書き起こしてください。" "中国語の発音が混入していないか、読み間違いがあれば指摘。" ])
全クリップに対して実行し、問題があるクリップのみ再生成。
ナレーション尺がシーン尺を超える場合、ffmpeg atempo で調整:
bash# 例: 5.1秒のナレーションを3.6秒に収める(atempo 1.42) ffmpeg -y -i raw.mp3 -af "atempo=1.42" adjusted.mp3
限界: atempo 1.35倍が上限。超えると早口で聞き取りづらい → テキストを短縮するか、シーン尺を延長。
typescript// props にナレーションプレフィックスを定義 interface Props { narrationPrefix?: string; // e.g. "tax/audio/narration/v4_s" } // 各シーンに Audio を配置 {p.narrationPrefix && starts.map((st, i) => { const padNum = String(i + 1).padStart(2, "0"); return ( <Sequence key={`narr-${i}`} from={st} durationInFrames={frames[i]}> <Audio src={staticFile(`${p.narrationPrefix}${padNum}.mp3`)} volume={1.2} /> </Sequence> ); })}
音量バランス: ナレーション volume={1.0-1.2}, BGM volume={0.18-0.25}
muted で BGM と干渉させない以下のテンプレートで全シーンを定義してから実装に入る:
| Scene | 秒数 | Beat数 | 参考クリップ | 演出概要 | コンポーネント名 | |-------|------|--------|-------------|---------|----------------| | 01 | 2.91 | 5 | {clip_id} | ロゴ blur→focus + パーティクル | LogoFocusIn | | 02 | 2.91 | 5 | {clip_id} | バリュー訴求パンチイン | ValuePunch | | 03 | 5.24 | 9 | {clip_id} | メンバー紹介ワイプ | MemberShowcase | | ... | | | | | |
typescriptinterface CompositionProps { // 全シーン共通 bgmSrc?: string; sectionDurations?: number[]; // Beat-aligned // シーン固有 props logoSrc?: string; // ... } export const DEFAULT_PROPS: CompositionProps = { sectionDurations: [2.91, 2.91, 5.24, 5.24, 12.23, 6.99, 8.74], // BPM倍数 bgmSrc: '{project}/audio/bgm.mp3', // ... };
sectionDurations 配列を先に確定 → 各シーンコンポーネントはフレーム数を受け取って内部で配分。Root.tsx の durationInFrames は Math.round(sum(sectionDurations) * FPS) で自動計算。
企業PV制作で確立した 13 のテクニック。新プロジェクトでもそのまま適用可能。
typescriptconst blurPx = interpolate(frame, [0, 20], [20, 0], { extrapolateRight: 'clamp' }); // style: { filter: `blur(${blurPx}px)` }
ロゴ画像を最初ぼかして徐々にシャープに。テキストでロゴを再現しない(フォント不一致リスク)。
typescriptconst scale = spring({ frame: f, fps, config: { damping: 12, mass: 0.5, stiffness: 200 } }); // style: { transform: `scale(${scale})`, opacity: Math.min(1, f / 5) }
テキストが弾むように出現。damping を低くしすぎると振動が目立つので 10-15 が適正。
typescriptconst progress = interpolate(frame, [startF, endF], [0, 100], { extrapolateRight: 'clamp' }); // style: { clipPath: `inset(0 ${100 - progress}% 0 0)` }
右→左に写真がワイプイン。方向は inset() の値を変えるだけ。
暗背景にランダム配置の白/色付き矩形を散らし、clipPath で写真を露出させる二段階演出。
typescript// Phase A: 矩形飛散(テキストは入れない) {rects.map((r, i) => ( <div key={i} style={{ position: 'absolute', top: r.y, left: r.x, width: r.w, height: r.h, background: r.color, transform: `rotate(${r.rot}deg)`, opacity: interpolate(f, [0, phaseAEnd], [1, 0]) }} /> ))} // Phase B: clipPath で写真ワイプ
画面を縦3分割し、各ストリップに異なるアングルの写真を配置。ストリップごとに微妙にタイミングをずらすとリッチに見える。
typescriptconst scrollY = interpolate(frame, [0, dur], [0, -totalHeight], { extrapolateRight: 'clamp' }); // グリッドを translateY でスクロール // 特定カードでデセラレーション → カーソル追従 → クリック演出
カーソル位置はスクロールオフセットからカードの実画面座標を逆算して合わせる。
typescriptconst panelDelay = panelIndex * 4; // フレーム単位のずらし const slideIn = interpolate(frame - panelDelay, [0, 15], [100, 0], { extrapolateRight: 'clamp' }); // 各パネルの baseOffset で異なる画像を表示 const imgIdx = (cycleIndex + pi * Math.ceil(images.length / 4)) % images.length;
4分割パネルの更新タイミングをずらす。全パネル同じ画像にならないよう baseOffset 必須。
typescript// spring() は振動するので絶対使わない const eased = 1 - Math.pow(1 - ratio, 3); // cubic ease-out const displayNum = Math.round(eased * targetNumber);
spring() でカウントアップすると数値が上がって下がって戻る。cubic ease-out を使うこと。
typescript// i2v の代替。position:absolute + CSS animation で浮遊ドット {particles.map((p, i) => ( <div key={i} style={{ position: 'absolute', borderRadius: '50%', width: p.size, height: p.size, background: p.color, top: `${p.y}%`, left: `${p.x}%`, opacity: p.opacity, animation: `float ${p.duration}s ease-in-out infinite`, }} /> ))}
i2v(Kling等)は品質が微妙 → CSS パーティクルやRemotionアニメーションで代替する。
typescriptconst exitProgress = interpolate(frame, [dur - exitFrames, dur], [0, 1], { extrapolateLeft: 'clamp' }); // style: { transform: `scale(${1 - 0.05 * exitProgress})`, opacity: 1 - exitProgress }
シーン終了時に scale: 0.95 + opacity: 0 で自然に次シーンへ遷移。noExit prop で無効化可能。
typescript// objectFit: "cover" は顔が切れる → "contain" + blurred background <div style={{ position: 'relative', overflow: 'hidden' }}> {/* ブラー背景 */} <Img src={src} style={{ position: 'absolute', width: '120%', filter: 'blur(20px)', objectFit: 'cover' }} /> {/* メイン画像 */} <Img src={src} style={{ position: 'relative', objectFit: 'contain', width: '100%', height: '100%' }} /> </div>
typescript// 黒帯はダサい → text-shadow で可読性確保 style: { textShadow: '0 2px 8px rgba(0,0,0,0.8), 0 0 20px rgba(0,0,0,0.5), 0 0 40px rgba(0,0,0,0.3)', // フォントサイズは大きめに(最低 48px) }
typescriptimport { OffthreadVideo } from 'remotion'; // <OffthreadVideo src={staticFile('{project}/video/live_muted.mp4')} muted />
ライブ映像等は muted で挿入し、BGM トラックと干渉させない。サムネイルは ffmpeg で事前抽出:
bashffmpeg -i video.mp4 -ss 5 -frames:v 1 thumb.jpg
bashcd mv-composer npx remotion render src/index.ts {CompositionId} out/{Name}_v{N}.mp4
bashffmpeg -i out/{Name}_v{N}.mp4 -vf "fps=8" data/video_refs/{project}/output_frames/v{N}_%04d.png
参考動画フレームと出力フレームを並べて、プロの映像クリエイター視点 で以下をチェック:
指摘事項を修正 → v{N+1} としてレンダリング → 再度比較。 v1→v18 のような反復は正常。5-10回のイテレーションで収束するのが標準。
| ルール | 理由 | |--------|------| | 絵文字をアイコンに使わない | レンダリング環境でフォント欠落 → SVG/画像を使う | | i2v(Kling/fal.ai等)に頼らない | 品質が微妙、Remotion CSS で代替可能 | | spring() でカウントアップしない | 振動して数値が上がって下がる | | objectFit "cover" で人物写真 | 顔が切れる → "contain" + blurred bg | | 黒帯テロップ | ダサい → text-shadow で可読性確保 | | 7MB超の画像をそのまま使う | デコードエラー → sips --resampleWidth 1200 | | テキストでロゴを再現 | フォント不一致 → ロゴ画像を使う |
| ルール | 理由 | |--------|------| | SE なし・BGM 一本 | 駆け抜ける勢いが出る | | sectionDurations を BPM 倍数に | 音ハメが自然になる | | 画像は 1200px 幅以下にリサイズ | Remotion のデコード安定性 | | シーン間を ±5フレーム オーバーラップ | 黒フレーム防止 | | 名前テロップは1シーン1回 | 2回出ると違和感 | | パネル画像は baseOffset で分散 | 全パネル同じ画像防止 | | ライブ映像は muted 挿入 | BGM との干渉防止 | | ブランドカラーは OGP/公式サイトから取得 | 目視推定は不正確 |
[1. Reference Research]
↓ 参考クリップ収集(5-10秒単位)
[2. Frame Analysis]
↓ フレーム抽出 → 演出分解
[3. BPM & Audio]
↓ テンポ解析 → sectionDurations 確定 → BGM 生成
[4. Storyboard]
↓ シーン分割表 → Props設計
[5. Implementation]
↓ パターン集(P1-P13)を活用して実装
[6. Comparison Loop] ←─── 5-10回反復
↓ レンダリング → フレーム比較 → 修正
[7. Final Check]
↓ Lessons Learned チェック → 完成| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-23 | pass→pass | 14,545 | 12,892 | -11% | 1 | 1 | 0% | 2,703 | 8,829 | +227% | 0 | 0 | — |
case-01 | fail→fail | 23,671 | 26,620 | +12% | 1 | 1 | 0% | 4,203 | 12,251 | +191% | 0 | 0 | — |
case-02 | fail→fail | 24,651 | 24,770 | +0% | 1 | 1 | 0% | 4,732 | 11,464 | +142% | 0 | 0 | — |
case-03 | fail→pass | 22,231 | 20,464 | -8% | 1 | 1 | 0% | 3,415 | 10,460 | +206% | 0 | 0 | — |
case-04 | fail→pass | 19,090 | 14,728 | -23% | 1 | 1 | 0% | 3,682 | 8,977 | +144% | 0 | 0 | — |
case-05 | fail→pass | 16,536 | 12,045 | -27% | 1 | 1 | 0% | 2,800 | 8,697 | +211% | 0 | 0 | — |
case-06 | fail→fail | 19,138 | 17,463 | -9% | 1 | 1 | 0% | 3,097 | 9,280 | +200% | 0 | 0 | — |
case-07 | fail→pass | 18,618 | 5,901 | -68% | 1 | 1 | 0% | 3,193 | 7,489 | +135% | 0 | 0 | — |
case-08 | fail→pass | 15,447 | 9,598 | -38% | 1 | 1 | 0% | 2,524 | 8,141 | +223% | 0 | 0 | — |
case-09 | fail→pass | 7,877 | 7,240 | -8% | 1 | 1 | 0% | 1,336 | 7,623 | +471% | 0 | 0 | — |
case-24 | pass→pass | 10,727 | 8,618 | -20% | 1 | 1 | 0% | 2,035 | 8,064 | +296% | 0 | 0 | — |
case-10 | fail→pass | 16,239 | 13,121 | -19% | 1 | 1 | 0% | 2,580 | 8,673 | +236% | 0 | 0 | — |
case-11 | fail→pass | 13,975 | 13,359 | -4% | 1 | 1 | 0% | 2,185 | 8,917 | +308% | 0 | 0 | — |
case-12 | pass→pass | 17,393 | 12,170 | -30% | 1 | 1 | 0% | 2,823 | 8,643 | +206% | 0 | 0 | — |
case-13 | fail→fail | 16,730 | 9,620 | -42% | 1 | 1 | 0% | 2,839 | 8,029 | +183% | 0 | 0 | — |
case-14 | fail→pass | 14,613 | 12,033 | -18% | 1 | 1 | 0% | 2,552 | 8,450 | +231% | 0 | 0 | — |
case-15 | fail→pass | 22,067 | 13,454 | -39% | 1 | 1 | 0% | 4,168 | 8,911 | +114% | 0 | 0 | — |
case-16 | fail→pass | 16,214 | 17,916 | +10% | 1 | 1 | 0% | 2,559 | 9,033 | +253% | 0 | 0 | — |
case-17 | fail→pass | 13,725 | 10,251 | -25% | 1 | 1 | 0% | 2,513 | 8,168 | +225% | 0 | 0 | — |
case-22 | pass→pass | 21,638 | 21,279 | -2% | 1 | 1 | 0% | 4,149 | 10,431 | +151% | 0 | 0 | — |
case-18 | fail→pass | 13,608 | 6,868 | -50% | 1 | 1 | 0% | 2,206 | 7,577 | +243% | 0 | 0 | — |
case-19 | pass→pass | 17,262 | 15,118 | -12% | 1 | 1 | 0% | 2,745 | 9,122 | +232% | 0 | 0 | — |
case-20 | pass→pass | 13,261 | 11,988 | -10% | 1 | 1 | 0% | 2,036 | 8,221 | +304% | 0 | 0 | — |
case-21 | pass→pass | 13,284 | 7,283 | -45% | 1 | 1 | 0% | 2,184 | 7,637 | +250% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 24 cases were attempted. The headline lift of +54 percentage points is the difference between those two pass rates over the 24 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.