mcpField
V
vtake-cut

vtake-cut

github.com/noteditクリエイティブ・メディア12056 回インストール21 週間前に更新
リポジトリ

vtake-cut について

VTake は、ローカル動画をカード形式の構成に変換し、MP4 にレンダリングするスキルと CLI ワークフローです。文字起こしに基づいてカードを設計し、HTML で作成します。中間ファイルも確認できます。

主な機能

  1. 01音声と動画メタデータの抽出
  2. 02文字起こしと内容の修正
  3. 03文字起こしに基づくカード設計
  4. 04カードと構成用 HTML の作成
  5. 05構成を MP4 にレンダリング

活用シーン

  1. 01ローカル動画をカード形式の編集にする
  2. 02文字起こしからビジュアルな絵コンテを作る
  3. 03タイムスタンプ付き文字起こしを確認・修正する
  4. 04組み立てた HTML を MP4 にレンダリングする

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/notedit/vtake-skills --skill vtake-cut

インストール後、Claude Code で/vtake-cutを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

assets/vendor/
gsap.min.js
references/
DESIGN_INDEX.md
references/frames/
clean.html
hairline.html
polaroid.html
references/layouts/
overlay.html
pip.html
split.html
stack.html
references/styles/
academic.html
audit.html
editorial.html
geom.html
minimal.html
spotlight.html
swiss.html
terminal.html
whiteboard.html
xhs.html

詳細

VTake ローカルワークフロー

VTake は、ローカルの入力動画をカードベースのコンポジションに変換します。エージェントがカード(タイミング+コンテンツ)を設計し、会話内に各カードの HTML を直接記述してから、単一のコンポジション HTML にまとめ、hyperframes を使って MP4 にレンダリングします。固定のアーキタイプ一覧や指定されたカード構造はありません。カードは、文字起こしの実際の内容に基づいて形作られます。

作業ディレクトリ内で確認できる中間ファイル:

  • metadata.json — 長さ/幅/高さ/fps
  • audio.mp3 — 抽出した音声
  • transcript.json — タイムスタンプ付きのセグメントと単語
  • storyboard.json — 軽量なカードの概要(エージェントの計画)
  • public/cards/card-XX.html — カードごとの HTML フラグメント
  • public/index.html — 最終的に組み立てたコンポジション
  • output.mp4 — レンダリングした動画

CLI の解決

# vtake CLI — auto-downloaded from npm on first run
npx -y @notedit/vtake@latest --help

# hyperframes — for rendering the assembled HTML to MP4
npx hyperframes render --help

以下の各 vtake … コマンドは、npx -y @notedit/vtake@latest … の省略表記です。

ワークフロー

1. 環境を確認する
npx -y @notedit/vtake@latest doctor
# confirm bundled assets:
ls "<SKILL_DIR>/assets/fonts" "<SKILL_DIR>/assets/vendor/gsap.min.js"

必須:

  • ffmpeg / ffprobe(システム)
  • <SKILL_DIR>/assets/fonts/*.woff2、<SKILL_DIR>/assets/vendor/gsap.min.js(このスキルにバンドル済み。ステップ 9 で作業ディレクトリに配置)

任意:

  • ELEVEN_API_KEY — 設定すると、vtake transcribe は ElevenLabs に直接接続し、レート制限付きプロキシを経由しません。設定されていない場合は、https://vtake.app/api/transcribe にフォールバックします。このプロキシでは、IP ごとに1 分あたり 3 件のリクエストが許可されています。VTAKE_TRANSCRIBE_ENDPOINT を使ってプロキシ URL を上書きできます(例:ローカルの Wrangler 開発環境用)。

macOS で hyperframes render を使用する場合は、次の設定を強く推奨します。

export PRODUCER_BROWSER_GPU_MODE=hardware
2. 作業ディレクトリを作成する
VIDEO_PATH="/absolute/path/input.mp4"
WORK_DIR=".vtake-work/$(basename "$VIDEO_PATH" | sed 's/\.[^.]*$//')"
mkdir -p "$WORK_DIR"
3. 音声とメタデータを抽出する
npx -y @notedit/vtake@latest extract "$VIDEO_PATH" --out-dir "$WORK_DIR"

出力:metadata.json(長さ、幅、高さ、fps)+ audio.mp3。

4. 文字起こしする
npx -y @notedit/vtake@latest transcribe "$WORK_DIR/audio.mp3" --out-dir "$WORK_DIR" --asr elevenlabs

出力:{ segments, words, raw } を含む transcript.json。

レート制限(プロキシモードのみ — ELEVEN_API_KEY なし): サーバーでは、IP ごとに 1 分あたり 3 件のリクエストが許可されています。rate_limited: または service_busy: で始まるエラーが表示された場合は、自動で再試行しないでください。処理を止め、待機する秒数をユーザーに伝えます(メッセージには再試行の目安が含まれています)。ユーザーから再度依頼されたら、このステップから再開します。

5. 文字起こしを修正する

transcript.json を読み、明らかな ASR エラーを修正します。

  • 同音異義語、製品名、専門用語、句読点
  • すべての start / end タイムスタンプを保持する
  • 可能な限り segments[].text のみを編集する
  • 明確な一対一の置き換えに限り、個別の words[].word を編集する
6. 軽量なストーリーボードを作成する(チャット内)

CLI は使用しません。 transcript.json と metadata.json を読み、カードを直接設計します。storyboard.json はエージェント内部の計画用成果物です。これを使用する vtake CLI コマンドはありません。ステップ 9 で各カードの HTML を記述する前に、タイミングとコンテンツを明確に検討できるように用意されています。以下の例と同じ構造にして、同じ概要からステップ 9 で作成するコンポジションを組み立てられるようにします。

{
  "schemaVersion": 3,
  "composition": {
    "fps": 30,
    "width": 1080,
    "height": 1920,
    "durationSeconds": 121.2,
    "layout": "portrait",
    "themeId": "noir",
    "seed": 42
  },
  "videoTrack": {
    "sourcePath": "input-video.mp4",
    "startSec": 0,
    "endSec": 121.2,
    "bounds": { "x": 0, "y": 0, "width": 1080, "height": 1920 }
  },
  "subtitles": { "enabled": false },
  "cards": [
    {
      "id": "card-01",
      "intent": "Hook with the speaker's anxious midnight question",
      "startSec": 0.5,
      "endSec": 13.0,
      "accentIndex": 0,
      "zone": "fullscreen",
      "contentHints": {
        "kicker": "AN HONEST QUESTION",
        "title": "晚上 11 点的灵魂提问",
        "detail": "客户六十秒语音:「人民币会升值,我的美金保单是不是亏惨了?」"
      }
    }
  ]
}

必須のカードフィールド:

フィールド型目的
idstringカード HTML と GSAP セレクターで使用する安定した ID
intentstring自然言語による説明。カードの生成に使用
startSec / endSecnumber秒単位の時刻(endSec > startSec)
accentIndex0 | 1 | 2 | 3 | 4このカードで使用する 5 色のテーマアクセントカラーの番号
zoneenum(下記参照)キャンバス上でカードを配置する場所
contentHintsobject自由形式のデータ一式。エージェントが kicker/title/detail/data/quote をここに記述
archetype(任意)stringカードのパターンを記憶しておくために付けられる自由形式のラベル。省略時はデフォルトの自由形式
transition(任意)enum:cut | fade | slide | wipeカード間の遷移を宣言

5 つの zone 値:

ゾーン解決後の境界使用する場面
fullscreenキャンバス全体を覆います印象的な演出、大きな数字、マントラ
whiteboard-area40px の余白(または縦向きの高さの 45%)を取りますデータ量が多い内容/注釈付きコンテンツ
lower-third下部 30% の帯状領域表示中の動画に重ねる注釈
side-panel右側 42%(横向き)または下部 40%(縦向き)片側にデータ、もう片側に動画を表示
video-overlayキャンバス全体を覆い、ほぼ透明なカードを想定します全画面動画に重ねる注釈

ステップ 9 でコンポジションを組み立てる際、上の表に従って各カードの zone をカードホストのラッパー上のピクセル境界に変換します。 動画の境界はコンポジションレベル(videoTrack.bounds)で一度だけ設定します。 動画を「カード間で移動」しているように見せるには、コンポジションの <script> 内で #video-wrap を対象に GSAP のトゥイーンを作成します(ステップ 9 を参照)。

**カードの役割や物語の展開に決まりはありません。**カードは動画で実際に語られている内容から生まれます。引用だけの場合も、データだけの場合もあり、数字から始まる場合も、物語から始まる場合もあります。トランスクリプトに合わせてリズムを作りましょう。

要点はいくつにするか? — 長さと密度から自動推定します。上限はありません。まず動画の長さから基本ペースを選び、次に情報密度に応じて調整します。固定されているのは最低数だけで、カードは最低 5 枚です。短い動画でもリズムを作れるようにします。

ステップ 1 — 長さに応じた基本ペース(中程度の密度における、カード 1 枚あたりの標準秒数):

動画の長さ基本ペース(カード 1 枚あたりの秒数)理由
< 60s(短いリール)6–8s短尺動画ではテンポの速いカットが期待されます
60s – 3 min8–12s一般的なソーシャル動画のペースです
3 – 10 min12–20s余裕を持たせ、各カードに多くの内容を盛り込みます
10 – 30 min20–35s長尺の講義/インタビューに適したリズムです
> 30 min30–60sエピソード形式で、章に近い感覚になります

ステップ 2 — 密度の乗数(基本ペースに掛けます):

トランスクリプト内の兆候乗数効果
高密度 — 数字が多い、異なる主張が次々と出てくる、テンポが小刻み、列挙形式、1〜2 文ごとに新しいアイデアが出てくる× 0.7カットが速くなり、カードが増えます
中程度の密度 — データと物語が混在する流れ× 1.0基本ペースです
低密度 — 長い物語が 1 つ続く、表現を変えて同じ内容を繰り返す、ゆっくり内省的に進む、1 つの主張を展開する× 1.5カットが遅くなり、カードが減ります

ステップ 3 — 計算:

secPerCard = basePace × densityMultiplier
cardCount  = max(5, round(videoDurationSec / secPerCard))

例(上限はありません。動画が長ければ、自然にカードも増えます):

  • 30s のリール、オチが 1 つ(低密度) → 7 × 1.5 = 1 カードあたり 10.5s → round(30/10.5)=3 → 最低数の 5 カードに調整
  • 60s の内省的な独白(低密度) → 10 × 1.5 = 1 カードあたり 15s → 4 → 最低数の 5 カードに調整
  • 121s のデータ豊富なトーク動画(高密度) → 10 × 0.7 = 1 カードあたり 7s → 17 カード
  • 5 min のインタビュー(中程度の密度) → 16 × 1.0 = 1 カードあたり 16s → 19 カード
  • 10 min の詳細解説(高密度) → 16 × 0.7 = 1 カードあたり 11s → 55 カード
  • 30 min の講義(中程度の密度) → 28 × 1.0 = 1 カードあたり 28s → 64 カード
  • 1 hr のポッドキャスト(低密度) → 45 × 1.5 = 1 カードあたり 67.5s → 53 カード

カードの表示時間が約 15s を超える場合は、内容を充実させたカード(データブロック、 複数段階の表示、段階的なアニメーションで展開する複数のサブポイントなど)を計画します。 静的な一行表示は 8s を超えると退屈になります。カードの多くが 30s を超える長い動画では、 タイムラインを複数のサブコンポジションに分割することを検討してください(章ごとに 1 つの .html を作成し、 data-composition-src でマウントします)。そうすることで、ファイルごとの GSAP タイムラインを 管理しやすくできます。timeline_track_too_dense HyperFrames lint 警告を参照してください。

content はプレーンな文字列(「标题:年化 5.69%\n说明:...」)でも、データを表現する任意の JSON 形式でもかまいません。各カードに適した形式はエージェントが決定します。

最後のカード — 必ず card-cta-vtake を追加します。

すべてのコンテンツカードの後に、固定のブランドアウトロカードを 1 枚 cards 配列に追加します:

{
  "id": "card-cta-vtake",
  "intent": "Powered By vTake brand outro",
  "startSec": <last content card's endSec>,
  "endSec":   <last content card's endSec + 2.0>,
  "accentIndex": 3,
  "zone": "fullscreen",
  "contentHints": {}
}

また、composition.durationSeconds を2.0秒延長し、card-cta-vtake.endSec と同じ値に設定します。ソース動画のトラックは動画の自然な終端で停止します。2秒の延長部分は、CTA カードの背後に動画が表示されない、純粋なグラフィックによるブランド演出です。CTA カードには、1秒のシネマティックな登場シーケンス(「Editorial Cinema」)を適用し、その後約0.7秒間表示してから、約0.3秒かけてフェードアウトします。

7. レンダリング戦略を決める
ユーザーにビジュアルの方向性を確認する(最初に実行すること)

カードのデザインや範囲の決定を始める前に、出力比率、レイアウト、スタイル、カード密度のプリセットをユーザーに選んでもらってください。フレームは、選択されたレイアウトとスタイルの組み合わせに基づいて自動選択されます(下記の「フレームの自動選択」表を参照)。質問を送る前に、次の2つを事前に計算してください。

  1. ソース動画のアスペクト比(metadata.json の幅 / 高さ)から recommendedRatio を計算します。

    • sourceAspect = width / height
    • sourceAspect ≥ 1.5(約3:2以上の横長)→ 16:9 を推奨
    • sourceAspect ≤ 0.7(約9:13以下の縦長)→ 9:16 を推奨
    • 0.7 < sourceAspect < 1.5(正方形に近い)→ 4:5 を推奨

    推奨する選択肢のラベルに「 (推荐 · 匹配源视频 X:Y)」を付け、推奨理由をユーザーに示します。

  2. Step 6 の autoCount(max(5, round(videoSec / (basePace × densityMultiplier))))を計算し、「自動」の選択肢のラベルに具体的な数を表示できるようにします。

環境との互換性 — 最適な質問方法を選んでください。 すべての実行環境で同じ構造化質問ツールが使えるとは限りません。次の順序で適用してください。

  1. AskUserQuestion(Claude Code、Anthropic Console)— 以下の構造化された4つの質問を使用します。
  2. その他のネイティブな確認ツール(例:ask_question、request_user_input、IDE 固有のプロンプト)— 同じ4つの質問文と選択肢リストを使って、そのツールで質問します。推奨マーカーと事前計算した値も維持してください。
  3. ネイティブツールがない場合(Codex CLI、プレーンテキストのみの実行環境)— 通常の会話で直接質問してください。このセクション末尾のプレーンテキスト用テンプレートを使います。1つのメッセージに、番号付きの質問を4つ含めてください(1ラウンドあたりの上限は2~5問です。この場合はその範囲内です)。

すべての質問方法に適用されるルール:

  • 1ラウンドにつき2~5問まで質問できます。ここでは4問なので問題ありません。
  • 不足情報がなくてもレンダリングを進められる場合でも、最終出力に大きく影響するパラメーター(比率、レイアウト、スタイル、cardCount)は、一度ユーザーに確認してください。
  • ユーザーがデフォルトを事前承認している(「just use defaults」、「无需询问」、「auto-pick everything」など)、または質問しないよう指示している場合は、質問を完全に省略し、recommendedRatio、layout="stack"(比率を問わず最も安全なデフォルト)、トランスクリプトのトーンに基づいて最も中立的なグループ(editorial/数据)から選んだ style、autoCount を使用します。選択した内容を1文でユーザーに伝えてから続行してください。

チャンネルA — ネイティブの AskUserQuestion:

// Precompute before the call:
//   recommendedRatio = "16:9" | "9:16" | "4:5"
//   autoCount        = integer (from Step 6)

AskUserQuestion({
  questions: [
    {
      question: "输出视频比例 (画幅):",
      header: "画幅",
      multiSelect: false,
      // Reorder so the recommended option appears FIRST (per AskUserQuestion convention).
      // Append " (推荐 · 匹配源视频 W×H)" to the recommended option's label.
      options: [
        { label: "16:9 (1920×1080) 横屏", description: "TV / YouTube / 电脑播放。源视频已经是横屏时最自然,画幅最宽。" },
        { label: "9:16 (1080×1920) 竖屏", description: "抖音 / 小红书 / TikTok / Reels。源视频竖屏时最自然;移动端原生体验。" },
        { label: "4:5 (1080×1350) 方屏偏竖", description: "Instagram feed / 微信朋友圈。近方形源视频或想兼顾两种平台时最稳。" }
      ]
    },
    {
      question: "选择整体布局:视频和卡片在画面里如何共存?",
      header: "布局",
      multiSelect: false,
      options: [
        { label: "左右分屏 (split)",     description: "video 和 card 各占画面一半。访谈 / 数据并列时最稳,画面分隔清晰。" },
        { label: "上下分屏 (stack)",     description: "video 在上方 (~52%),card 在下方。说话人头像 + 总结句的经典组合,竖屏也好用。" },
        { label: "画中画 (pip)",         description: "card 满屏,video 缩成圆角小窗在右上角。内容为主、speaker 为辅时用。" },
        { label: "全屏浮层 (overlay)",   description: "video 全屏播放,card 作为玻璃浮层落在画面上。情绪 / 电影感强烈。" }
      ]
    },
    {
      question: "选择卡片视觉风格 (style):",
      header: "风格大类",
      multiSelect: false,
      // NOTE: these 3 groups intentionally match the frame auto-pick matrix
      // rows below, so picking a group resolves both `style` group AND the
      // frame matrix column in one step. Memberships are mutually exclusive.
      options: [
        { label: "温暖纸感 (warm-paper)", description: "academic 学术笔记 · editorial 大字编辑 · whiteboard 手写白板 · xhs 小红书。适合访谈反思、产品发布、生活方式、情绪故事。" },
        { label: "冷峻临床 (clinical)",   description: "audit 审计杂志 · swiss 瑞士网格 · terminal CLI · minimal 现代极简。适合财报分析、调查报告、技术教程、严肃陈述。" },
        { label: "实验前卫 (experimental)", description: "geom 撞色几何 · spotlight 暗色聚光。适合短视频高光、产品发布、强烈情绪、电影质感。" }
      ]
    },
    {
      question: "卡片数量 (takeaway 节奏):要切多少张?",
      header: "卡片数量",
      multiSelect: false,
      options: [
        { label: "自动 (推荐) · 约 N 张", description: "按视频时长和信息密度自动推断 (见 Step 6 规则)。本次推断约 N 张。带 N 进 label —— N 是你刚算出的 autoCount。" },
        { label: "少量 · 约 round(N × 0.6) 张", description: "切得稀疏一点,每张卡停留更久,适合 reflective / 慢节奏。" },
        { label: "更多 · 约 round(N × 1.5) 张", description: "切得更紧凑,节奏更快,适合 staccato / 数据密集 / 短视频高光。" }
      ]
    }
  ]
})

「Other」について — AskUserQuestion は「カード数」の質問に「Other」の選択肢を自動で追加します。ユーザーは数字(例:「8」、「20」)を直接入力して、目標の cardCount を指定できます。入力を整数として解析してください。 解析に成功した場合 → その値をそのまま使用します(最低5枚を下限とします)。解析に失敗した場合 → 「自動」に戻します。

チャンネルB — プレーンテキストでの代替手段(Codex CLI、ネイティブな質問ツールがない実行環境)。以下を通常のメッセージとして1つ投稿し、返信を待ちます。箇条書き形式の1/2/3/4なら、返信を解析しやすくなります。

我需要先和你确认四个视觉决策再开始切卡片:

1) 输出比例 (画幅):
   A. 16:9 横屏 (1920×1080) — TV / YouTube / 电脑播放
   B. 9:16 竖屏 (1080×1920) — 抖音 / 小红书 / TikTok
   C. 4:5 方屏偏竖 (1080×1350) — Instagram feed / 兼顾两端
   ▸ 我的推荐:  <recommendedRatio>  (匹配源视频 W×H = <sourceW>×<sourceH>)

2) 整体布局 (video & card 怎么共存):
   A. split   左右分屏 (50/50)
   B. stack   上下分屏 (video 顶, card 底)
   C. pip     画中画 (card 满屏, video 圆角小窗)
   D. overlay 全屏浮层 (video 全屏, card 玻璃浮层)

3) 卡片风格大类 (与 frame 自动矩阵同构,3 选 1):
   A. 温暖纸感 warm-paper      (academic / editorial / whiteboard / xhs)
   B. 冷峻临床 clinical        (audit / swiss / terminal / minimal)
   C. 实验前卫 experimental    (geom / spotlight)

4) 卡片数量 (takeaway 节奏):
   A. 自动 (推荐) — 约 <autoCount> 张
   B. 少量 — 约 round(<autoCount> × 0.6) 张
   C. 更多 — 约 round(<autoCount> × 1.5) 张
   D. 直接给我一个数字 (如 "8"、"20")

回复格式: "1A 2C 3B 4A" 或自然语言均可。
若你想全部用推荐默认值,回复 "默认" / "auto" / "都用推荐" 即可。

プレーンテキストの返信の解析:

  • "1A 2C 3B 4A"、"A C B A"、"16:9 / pip / 数据 / 自动"、文章による回答、または 默认 など、形式は柔軟に受け付けます。
  • 回答に曖昧な点がある場合 → 曖昧な項目だけを再質問します(引き続き2~5問の上限内に収めます)。
  • ユーザーが「默认 / auto / 都用推荐」と言った場合 → 再質問せずに省略します。

ユーザーが回答した後(いずれの方法でも):

  1. 比率の回答に基づいて出力キャンバスを決定します。以下の値を storyboard.composition.width / height に正確に書き込みます。

    ユーザーの選択composition.width × heightstoryboard.layout フィールド
    16:91920 × 1080"landscape"
    9:161080 × 1920"portrait"
    4:51080 × 1350"portrait"(スキーマでは4:5をポートレートとして扱います — 高さが幅を上回ります)
    references/layouts/*.html 内で 4:5 の範囲を指定する場合 — これらのファイルでは、
    横長(1920×1080)と縦長(1080×1920)のみを扱っています。4:5(1080×1350)の
    範囲は、縦長から比例換算して求めます。水平方向の値はそのままにし、垂直方向の値を
    1350/1920 ≈ 0.703 倍します。例:overlay の縦長カードが
    { x: 24, y: 1280, w: 1032, h: 564 } の場合、4:5 のカードは
    { x: 24, y: round(1280 × 0.703), w: 1032, h: round(564 × 0.703) }
    = { x: 24, y: 900, w: 1032, h: 397 } です。
  2. スタイルグループを具体的なスタイルに割り当てます。 文字起こしのトーンを 確認して、最も合うものを選びます。ただし、ユーザーが選んだグループの範囲内に 収めてください。グループ内の具体的なスタイルを 2 つに絞りきれない場合は、 その 2〜4 個の具体的なスタイル候補を提示して、2 回目の AskUserQuestion を送ります。

  3. 密度の回答から最終的な cardCount を決定します。

    ユーザーの選択最終的な cardCount
    自动 (推荐)すでに計算した autoCount
    少量max(5, round(autoCount × 0.6))
    更多round(autoCount × 1.5)(上限による制限なし)
    Other = ""(整数)max(5, parseInt(n))
    Other = その他autoCount にフォールバック
  4. 次の表から動画フレームを自動選択します(フレームについてユーザーには尋ねません。レイアウトとスタイルから決まります)。

    レイアウトwarm-paper スタイル(academic / whiteboard / editorial / xhs)clinical スタイル(audit / swiss / terminal / minimal)experimental スタイル(geom / spotlight)
    splitpolaroidhairlineclean
    stackpolaroidhairlineclean
    pipclean(pip のピルにはすでに装飾があるため)cleanclean
    overlayclean(フルブリードでは装飾フレームを使用できないため)cleanclean
  5. 選択した内容を、アスペクト比(+キャンバスサイズ)、レイアウト、具体的なスタイル、 フレーム、最終的な cardCount を含めて 1 文でユーザーに伝えます。その後、ステップ 7 の 残りの作業(カードごとのレイアウト、モーションパターン)に進みます。

  6. 5 つの値(ratio / layout / style / frame / cardCount)を作業メモリに記録します (スキーマフィールドは不要です)。ステップ 8 で各カードの HTML を記述する際と、 トークンおよび構造を確認するために対応する references/<dim>/<key>.html を読む際に 参照します。

ユーザーが「Other」で 10 種類のスタイルライブラリにないスタイル名を自由入力した場合は、 それを新しいカードのビジュアルを独自にデザインするためのヒントとして扱います。ただし、 選択されたレイアウトの範囲を基準にしてください。

レンダリング戦略の入力

Step 7.0 で ratio / layout / style / cardCount / frame を固定したら、 カードごとに決める項目は次のとおりです。

  • GSAP のターゲット内でのソース動画のフィット方法:動画要素には object-fit: cover が設定され、#video-wrap のトゥイーン範囲に クリップされます。 トリミングしたくない場合(たとえば、縦向きのソース動画を横向きの キャンバスに配置する際に上下を切りたくない場合)は、ソースと同じ アスペクト比の矩形をトゥイーンのターゲットにし、周囲のキャンバスを 見せる(またはカード/背景で埋める)ようにします。
  • カードごとの card.zone:選択した構成レイアウトから決めます (split → side-panel、stack → lower-third、pip → fullscreen、 overlay → video-overlay)。または、個別のバリエーションとして 別のゾーンを選びます(ヒーロー/引用には fullscreen、高密度の データには whiteboard-area)。
  • カードごとの accentIndex:各カードでは、テーマのアクセントカラー 5色のうち1色を使います。リズムを生むためカードごとに変え、同じ ナラティブ上のまとまりに属するカードには同じインデックスを再利用します。
  • モーションの語彙:data-anim の種類から、繰り返し使うパターンを 2~3種類選び(後述の表を参照)、構成に一貫性を持たせるため、それらを 継続して使います。

次の themeId パレットから選び、構成の <style> ブロックで --accent-N/--bg/--text CSS 変数として使用します。

themeIdアクセントパレット(5色)ボードの背景テキスト
classic#1971c2 #e03131 #2f9e44 #e8590c #9c36b5#FFF9E3(紙)#1e1e1e
noir#4cc9f0 #f72585 #4ade80 #fb923c #a78bfa#1a1a1a#f1f1f1
mint#0077b6 #d62828 #2d6a4f #e76f51 #7209b7#e8faf0#1b4332
craft#bf5700 #d62728 #6c757d #e9b54a #3d5a80#f6efe1#2d2d2d
slate#0ea5e9 #ef4444 #22c55e #f97316 #a855f7#1e293b#f1f5f9
mono#000 #555 #888 #aaa #ccc#fff#000

使用可能なフォント(<SKILL_DIR>/assets/fonts/ 内の woff2 ファイル。Step 9 で 作業ディレクトリに配置されます):Caveat(手書き風)、 LXGW WenKai TC(中国語の手書き風)、Inter(モダンなサンセリフ)、 Virgil(幾何学的な手書き風)。@font-face または font-family で 直接参照します。

ビジュアルパターンの参考として、<SKILL_DIR>/references/styles/ には 10種類の自己完結型リファレンスカード(学術/エディトリアル/ミニマル/ スポットライト/ジオメトリック/ホワイトボード/監査/ターミナル/ スイス/xhs)が用意されており、出発点としてコピーできます。ただし、 これらに合わせる必要はありません。各カードは独自にデザインしてください。

ビジュアルデザインライブラリ(<SKILL_DIR>/references/)

構成レベルの themeId に加えて、このスキルには <SKILL_DIR>/references/ に、自由に組み合わせられる3つの独立したビジュアル要素を網羅した、より充実したリファレンスライブラリが含まれています。

Style  ×  Layout  ×  VideoFrame
 (10)      (4)         (3)
要素キー決定する内容
styleacademic editorial minimal spotlight geom whiteboard audit terminal swiss xhsカードのビジュアル表現 — フォント、色、装飾、カード内のレイアウト
layoutsplit stack pip overlayキャンバス上でソース動画とカードをどのように配置するか
frameclean hairline polaroid動画要素の周囲の装飾的な枠

全体の一覧と大まかな選択ガイド(インタビュー/製品発表/データ分析/ソーシャル向け編集/技術チュートリアル/感情を伝えるストーリーなど)については、<SKILL_DIR>/references/DESIGN_INDEX.md を参照してください。特定のスタイル/レイアウト/フレームを使うと決めたら、対応するファイルを参照してください。

  • references/styles/<key>.html — そのスタイルのCSSトークン(色、フォント、余白、装飾)とプレースホルダーの要点を含む、単独で使用できるカードフラグメントです。.card[data-card-id="ref-<key>"] のスタイルブロックをコピーし、data-card-idをカードのIDに変更して、プレースホルダーの内容を実際の要点に差し替えれば完了です。
  • references/layouts/<key>.html — 横向きと縦向きの両方に対応した正確な videoBounds と cardBounds、および storyboard.json のカードごとの layout フィールドに貼り付けられるJSONスニペットです。
  • references/frames/<key>.html — #video-wrap の兄弟要素として追加する装飾的なHTMLと、構成CSSへの配置手順です。

カードごとに style × layout × frame を選択してください。切り替えが自然に見える限り、カードごとに3つすべてを変更できます。よくある流れは、editorial × overlay × clean で始め、データカードでは audit × split × hairline に切り替え、最後は whiteboard × pip × polaroid で締めくくる方法です。

10種類のスタイルはスキル側のデザイントークンであり、構成レベルのテーマではありません。storyboard.composition で宣言する必要はなく、各カードのHTML内に記述します。themeId フィールドでは引き続き構成レベルのパレットを選択できます。このパレットはページ本文の背景と動画の枠線を制御します(上の表を参照)。

レイアウト構成(カード+動画)

各カードについて、ソース動画とキャンバスをどう共有するかを、連動する2つの判断で定義します。

  • card.zone(storyboard.jsonで宣言) — スキーマで定められた5つの値のいずれかです。ステップ9でカードホストのラッパーにインラインstyleを書く際、ステップ6の表に従ってピクセル単位の境界に変換します。
  • このカードの時間枠における#video-wrapの境界(構成のGSAPタイムライン内で命令的に宣言) — エージェントは、レイアウト遷移ごとに#video-wrapを目標の矩形へトゥイーンします。

スキーマには、カードごとの動画境界は保存されません。videoTrack.boundsは構成レベルで一度だけ設定され(デフォルトはキャンバス全体)、カード間で動画が「移動する」のは、index.htmlに記述するGSAPアニメーションのみです。card.layoutフィールドはありません。このドキュメントの以前のバージョンでは誤って記載していましたが、実際のスキーマにあるのはcard.zoneだけです。

4つの構成レイアウト(references/layouts/にあります) — それぞれ、zoneと#video-wrapのトゥイーン目標を組み合わせたレシピです。

構成レイアウト推奨するcard.zone#video-wrapのGSAP目標(横向き1920×1080)#video-wrapのGSAP目標(縦向き1080×1920)使用する場面
splitside-panel{ left: 960, top: 0, width: 960, height: 1080 }{ left: 0, top: 960, width: 1080, height: 960 }(下半分)登壇者とデータを横並びにする/比率50:50
stacklower-third{ left: 14, top: 14, width: 1892, height: 548 }(上部52%){ left: 0, top: 0, width: 1080, height: 844 }(上部44%)登壇者を上、要約カードを下に配置する
pipfullscreen{ left: 1480, top: 760, width: 400, height: 300 } + .framedクラスを追加{ left: 690, top: 28, width: 360, height: 203 } + .framedを追加コンテンツ主体のカード+隅のピクチャーインピクチャー
overlayvideo-overlay{ left: 0, top: 0, width: 1920, height: 1080 }(全画面表示){ left: 0, top: 0, width: 1080, height: 1920 }映画的/ドラマチックな演出/動画全体に重ねるガラス風カード

4:5(1080×1350)の場合は、縦向きのy/hの値に1350/1920 ≈ 0.703を掛けて拡大縮小します(ステップ7.0のChannel A/Channel BのrecommendedRatio解像度表を参照)。

単発のバリエーションで使うその他のzone値(引き続きcard.zoneを使います。「layout」フィールドをでっち上げないでください)。

zone変換後の境界一般的な用途
fullscreenキャンバス全体を覆うヒーローカード。動画は非表示またはピクチャーインピクチャーにトゥイーンします
whiteboard-area40pxの余白(横向き)または下部45%(縦向き)を除いた範囲情報量の多いデータカード、余白を確保する場合
lower-third下部30%の帯トーク映像への注釈
side-panel右側42%(横向き)または下部40%(縦向き)サイドバー/「split」レシピ
video-overlayキャンバス全体。カードのルートは透明にする画面全体に表示する動画に重ねるガラス風オーバーレイ

カードごとにレシピを組み合わせられます。その場面に合うcard.zoneを選び、カード間で#video-wrapをトゥイーンするGSAPコードを記述してください。

ストーリーボードのレンダリング契約

storyboard.json はエージェント内部で使う計画用アーティファクトです。これを解析する vtake CLI コマンドはありません。各カードの HTML を書く前に、タイミングと内容の判断を明確にするために使います。Step 9 で組み立てる構成にも同じアウトラインを使えるよう、以下の v3 形式に沿ってください。

必須の構造(完全な例は Step 6 を参照):

  • schemaVersion: 3
  • composition: { fps, width, height, durationSeconds, layout, themeId, seed } — durationSeconds/fps/themeId/layout は最上位ではなく、composition 内に置きます
  • videoTrack: { sourcePath, startSec, endSec, bounds? } — 動画の境界は既定でキャンバス全体です
  • subtitles: { enabled, ... }
  • cards[] — 各カードには必須フィールドが 6 つあります:id、intent、startSec、endSec、accentIndex、zone、contentHints

ルール:

  • カードの時間は composition.durationSeconds の範囲内に収め、意図的でない限り重ねないでください。重ねる場合は data-track-index を使って z 順を制御します。
  • ビジュアルの詳細はカード HTML フラグメント(Step 8)に記述し、contentHints には記述しないでください。contentHints はカードのデザイン用に独自に作成する構造化プロンプトです。レンダリング後の見た目を決めるのは HTML です。
  • ストーリーボードの形式は安定させてください。解析されることはありませんが、Step 8/9 の作成時に読み返すため、一貫性を保つことでカード ID とタイミングの同期が取れます。
  • 「overlay × geom × clean を選んだ」といったエージェント側の判断は、storyboard.json に記述しないでください。作業用メモリに保持し、カード HTML と GSAP トゥイーンの作成時に使ってください。

動画と同じキャンバスに表示するカードは、背景を透明にします。 GSAP トゥイーンによってカードの背後または隣に動画が表示される場合(overlay レシピ、pip レシピ、または card.zone = 'lower-third' | 'video-overlay' のタイミング)、カードの .root に不透明な背景を全面に描画してはいけません。描画すると動画が隠れてしまいます。次の 2 つのパターンがあります。

/* Pattern A: transparent root, page body provides the cream backdrop */
html, body { background: var(--bg); }
.card[data-card-id="card-X"] .root { background: transparent; }

/* Pattern B: explicit per-card background ONLY for fullscreen cards */
.card[data-card-id="card-hero"] .root { background: var(--bg); }
.card[data-card-id="card-overlay"] .root { background: transparent; }

side-panel ゾーンのカード(split レシピ)の場合、card-host はキャンバスの半分のみを占めるため、不透明なカード背景を使っても問題ありません。背景が覆うのはその半分だけです。

8. 各カードの HTML を作成する

各カードについて $WORK_DIR/public/cards/{card-id}.html を作成します。各ファイルには、次の契約に従うルートが 1 つだけの HTML フラグメントを記述します。

カード HTML 契約
<div class="card" data-card-id="{cardId}">
  <style>
    /* MUST: every rule starts with .card[data-card-id="{cardId}"] */
    .card[data-card-id="card-01"] .root {
      width: 100%; height: 100%;
      display: flex; ...;
      font-family: 'Caveat', 'LXGW WenKai TC', serif;
      color: var(--text);
      background: var(--bg);
    }
    .card[data-card-id="card-01"] .title { font-size: 84px; ... }
  </style>

  <div class="root">
    <h1 id="card-01-title"
        data-anim="kinetic-chars"
        data-anim-at="0.3"
        data-anim-duration="0.5"
        data-anim-stagger="0.04"
        data-anim-pattern="pop">
      <span class="char">字</span>
      <span class="char">幕</span>
    </h1>
    <div id="card-01-line"
         data-anim="grow-x"
         data-anim-at="0.65"
         data-anim-duration="0.5"
         data-anim-target-w="420"
         style="width:0;height:8px;background:var(--accent-0);border-radius:4px;"></div>
  </div>
</div>

厳守事項(hyperframes の lint で違反があると拒否されます):

  • ルートの <div class="card" data-card-id="{cardId}"> は 1 つだけにします
  • インラインの <style> ルールには、上記のスコープセレクターを必ず付けます
  • <script> タグは禁止です
  • src=/href= に外部 URL を指定しないでください(CDN、リモートフォントは禁止)
  • インラインイベントハンドラー(onclick= など)は禁止です
  • すべてのアセットは、同じ public/ ディレクトリ内への相対パスで指定します
  • テーマを切り替えても使えるよう、色は var(--accent-N) などで指定します

アニメーションはコードに記述せず、宣言します。 data-anim-* 属性のみを使用し、アニメーション用に <script> を記述しないでください。すべての data-anim-* 宣言をコンパイルし、Step 9 で単一のマスター GSAP タイムラインにまとめます。

カードのサイズ設定 — 縦向きのモバイルファースト

references/styles/*.html の 10 個のファイルは、1920×1080 の横向き プレビュー用にサイズ設定されています。storyboard.layout = "portrait"(1080×1920。ソーシャル/モバイルで主流の ケース)の場合は、すべての視覚要素のサイズを大きくしてください。スマートフォンは画面を近くで見るため、同じピクセル数でも 横向きのテレビ風キャンバスより小さく見えます。

トークン横向きの基準値縦向きの目標値倍率
タイトル(ヒーロー h1/h2)64–96px88–132px×1.35
詳細/本文24–30px30–40px×1.30
キッカー/チップのラベル14–16px18–22px×1.30
タイムコード/メタデータ12–14px16–18px×1.30
データブロックの主数値48–60px64–88px×1.40
行の高さの倍率1.05–1.5同じ(拡大しない)

目安: portraitPx = round(landscapePx × 1.3) とし、視覚的なリズムを整えるため、 近い 4px 刻みの値に切り下げます。ヒーロー見出しは最大 ×1.4 まで拡大できます。小さな メタデータのテキストは、窮屈にならないよう ×1.2 にとどめます。

縦向きでは、パディングを少し小さくします。カードの幅が狭いため、横向きで大きな パディング(40–64px)を使うと幅を取りすぎます。縦向きでは、左右のパディングを 24–36px にしてください。

両方のレイアウトで使える単一のカードを作る場合は、サイズを固定値で指定するより、カードのルート要素に対する @container クエリを優先してください。

.card[data-card-id="X"] .root { container-type: inline-size; }
.card[data-card-id="X"] .title { font-size: clamp(64px, 8.5cqi, 132px); }
.card[data-card-id="X"] .detail { font-size: clamp(24px, 3.2cqi, 40px); }

ただし、ほとんどのカードではレイアウトを 1 つに決めて問題ありません。ストーリーボードの layout フィールドに 一致するサイズ表の列を選んでください。

使用できる data-anim の種類
種類用途主なパラメーター
fade-in表示at、duration、ease?
fade-out非表示at、duration、ease?
slide-inスライド表示at、duration、from=left|right|top|bottom、distance
kinetic-chars文字ごとのポップ表示at、duration、stagger、pattern=pop|fade — 要素には <span class="char"> の子要素が必要です
typewriter文字ごとのフェード表示kinetic-chars と同じですが、デフォルトのスタッガーはより遅くなります
count-up数値のアニメーションat、duration、from、to、format=.0f|.1f|.2f|,d
draw-pathSVG パスの描画表示at、duration — 要素は <path> にしてください
grow-yバーの高さat、duration、target-h(px)— 要素の開始時の height:0
grow-xバーの幅at、duration、target-w(px)— 要素の開始時の width:0
scale-popポップ表示at、duration
blur-inぼかしからフォーカスat、duration
mask-revealクリップ表示at、duration、direction=left|right|top|bottom
morph-to任意の CSS をトゥイーンat、duration、props='{...JSON...}'

data-anim-at は、カードの startSec からの相対秒数です。ステップ 9 で各宣言を GSAP タイムラインに コンパイルする際に、カードの startSec を加算して絶対時間を求め、1/fps に量子化してください。

card-cta-vtake:固定ブランドアウトロカード

この固定テンプレートを$WORK_DIR/public/cards/card-cta-vtake.htmlとして保存してください — 変更しないでください。デザイン言語は**「エディトリアル・シネマ」です。ビューファインダーのコーナーマーク、上下のフィルムクレジットメタストリップ、vTマークを囲む2つの回転リング、ダイヤモンドを両端に配した区切り線、イタリック体のタグライン、アンバー色のきらめくパーティクルを取り入れます。登場シーケンスは1.00s**で完了し、残り約1.0sは環境アニメーション(リングの回転、パーティクルの脈動)が続きます。


<div class="card" data-card-id="card-cta-vtake">
  <style>
    .card[data-card-id="card-cta-vtake"] .root {
      width: 100%; height: 100%;
      background:
        radial-gradient(ellipse 60% 80% at 50% 50%, rgba(232,144,52,0.12) 0%, transparent 55%),
        radial-gradient(ellipse 120% 40% at 50% 100%, rgba(201,169,97,0.10) 0%, transparent 60%),
        #0D0B08;
      position: relative; overflow: hidden;
      container-type: inline-size;
      font-family: 'Inter', ui-sans-serif, sans-serif;
      color: #F5EFE1;
    }
    /* fractal noise overlay — kills the "plastic" look */
    .card[data-card-id="card-cta-vtake"] .root::after {
      content: '';
      position: absolute; inset: 0;
      background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 1  0 0 0 0 1  0 0 0 0 1  0 0 0 0.06 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
      mix-blend-mode: overlay;
      opacity: 0.5;
      pointer-events: none;
    }
    /* viewfinder corner marks (1.5px hairline) */
    .card[data-card-id="card-cta-vtake"] .corner {
      position: absolute;
      width: 3.5cqi; height: 3.5cqi;
      border-color: rgba(245,239,225,0.18);
      pointer-events: none;
    }
    .card[data-card-id="card-cta-vtake"] .corner.tl { top: 3.5cqi; left: 3.5cqi; border-top: 1.5px solid; border-left: 1.5px solid; }
    .card[data-card-id="card-cta-vtake"] .corner.tr { top: 3.5cqi; right: 3.5cqi; border-top: 1.5px solid; border-right: 1.5px solid; }
    .card[data-card-id="card-cta-vtake"] .corner.bl { bottom: 3.5cqi; left: 3.5cqi; border-bottom: 1.5px solid; border-left: 1.5px solid; }
    .card[data-card-id="card-cta-vtake"] .corner.br { bottom: 3.5cqi; right: 3.5cqi; border-bottom: 1.5px solid; border-right: 1.5px solid; }
    /* top film-credit strip */
    .card[data-card-id="card-cta-vtake"] .top-meta {
      position: absolute; top: 4.8cqi; left: 0; right: 0;
      display: flex; justify-content: center;
      gap: clamp(14px, 2cqi, 36px);
      font-size: clamp(9px, 0.85cqi, 16px);
      font-weight: 500;
      letter-spacing: 0.36em;
      text-transform: uppercase;
      color: rgba(245,239,225,0.18);
    }
    .card[data-card-id="card-cta-vtake"] .top-meta .sep { color: #E89034; opacity: 0.7; }
    /* bottom film-credit strip */
    .card[data-card-id="card-cta-vtake"] .bot-meta {
      position: absolute; bottom: 5cqi; left: 0; right: 0;
      display: flex; justify-content: space-between;
      padding: 0 7cqi;
      font-size: clamp(9px, 0.8cqi, 15px);
      font-weight: 500;
      letter-spacing: 0.32em;
      text-transform: uppercase;
      color: rgba(245,239,225,0.18);
    }
    .card[data-card-id="card-cta-vtake"] .bot-meta .right { color: #E89034; opacity: 0.85; }
    /* center stage */
    .card[data-card-id="card-cta-vtake"] .stage {
      position: absolute; top: 50%; left: 50%;
      transform: translate(-50%, -50%);
      display: flex; flex-direction: column; align-items: center;
      z-index: 2;
    }
    /* "Powered by" row with short flanking lines */
    .card[data-card-id="card-cta-vtake"] .powered-row {
      display: flex; align-items: center;
      gap: clamp(10px, 1.4cqi, 24px);
      margin-bottom: clamp(24px, 3cqi, 56px);
    }
    .card[data-card-id="card-cta-vtake"] .powered-row .line {
      height: 1px;
      background: rgba(245,239,225,0.18);
      width: 0;
    }
    .card[data-card-id="card-cta-vtake"] .powered-row .label {
      font-size: clamp(11px, 1.05cqi, 20px);
      font-weight: 600;
      letter-spacing: 0.46em;
      text-transform: uppercase;
      color: rgba(245,239,225,0.40);
    }
    /* vT mark wrapper + dual concentric rings */
    .card[data-card-id="card-cta-vtake"] .mark-wrap {
      position: relative;
      width: clamp(160px, 15cqi, 280px);
      height: clamp(160px, 15cqi, 280px);
      display: flex; align-items: center; justify-content: center;
    }
    .card[data-card-id="card-cta-vtake"] .mark-ring {
      position: absolute; inset: 0;
      border-radius: 50%;
      border: 1.5px dashed rgba(232,144,52,0.22);
      animation: ctaRingSpin 32s linear infinite;
    }
    .card[data-card-id="card-cta-vtake"] .mark-ring.inner {
      inset: 8%;
      border: 1px solid rgba(245,239,225,0.10);
      animation: ctaRingSpin 32s linear infinite reverse;
    }
    .card[data-card-id="card-cta-vtake"] .vtake-mark {
      width: 60%; height: auto;
      color: #F5EFE1;
      position: relative; z-index: 1;
      overflow: visible;
    }
    /* main brand name */
    .card[data-card-id="card-cta-vtake"] .vtake-name {
      font-size: clamp(48px, 6cqi, 112px);
      font-weight: 900;
      letter-spacing: -0.05em;
      margin-top: clamp(20px, 2.5cqi, 48px);
      line-height: 1;
      overflow: hidden;
      clip-path: inset(0 100% 0 0);
    }
    .card[data-card-id="card-cta-vtake"] .name-accent { color: #E89034; }
    /* decorative divider: ── ◆ ── */
    .card[data-card-id="card-cta-vtake"] .divider {
      display: flex; align-items: center;
      gap: clamp(6px, 0.7cqi, 14px);
      margin-top: clamp(18px, 2.2cqi, 40px);
    }
    .card[data-card-id="card-cta-vtake"] .divider .seg {
      width: 0; height: 1px;
      background: rgba(232,144,52,0.5);
    }
    .card[data-card-id="card-cta-vtake"] .divider .diamond {
      width: clamp(4px, 0.45cqi, 8px);
      height: clamp(4px, 0.45cqi, 8px);
      background: #E89034;
      transform: rotate(45deg) scale(0);
    }
    /* tagline below divider */
    .card[data-card-id="card-cta-vtake"] .tagline {
      margin-top: clamp(14px, 1.8cqi, 32px);
      font-size: clamp(10px, 0.9cqi, 17px);
      font-weight: 500;
      letter-spacing: 0.56em;
      text-transform: uppercase;
      color: rgba(245,239,225,0.40);
      padding-left: 0.5em;
    }
    /* amber sparkle particles */
    .card[data-card-id="card-cta-vtake"] .spark {
      position: absolute;
      width: clamp(2px, 0.2cqi, 3px);
      height: clamp(2px, 0.2cqi, 3px);
      background: #E89034;
      border-radius: 50%;
      box-shadow: 0 0 14px #E89034;
      opacity: 0;
    }
    .card[data-card-id="card-cta-vtake"] .spark.s1 { top: 32%; left: 22%;  animation: ctaSparkle 3.5s ease-in-out 1.0s infinite; }
    .card[data-card-id="card-cta-vtake"] .spark.s2 { top: 64%; left: 76%;  animation: ctaSparkle 4.2s ease-in-out 1.4s infinite; }
    .card[data-card-id="card-cta-vtake"] .spark.s3 { top: 26%; right: 22%; animation: ctaSparkle 3.8s ease-in-out 1.6s infinite; }
    .card[data-card-id="card-cta-vtake"] .spark.s4 { bottom: 32%; left: 38%; animation: ctaSparkle 4.6s ease-in-out 0.8s infinite; }
    .card[data-card-id="card-cta-vtake"] .spark.s5 { top: 48%; right: 14%; animation: ctaSparkle 3.2s ease-in-out 2.0s infinite; }
    @keyframes ctaRingSpin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
    @keyframes ctaSparkle {
      0%, 100% { opacity: 0; transform: scale(0.5); }
      50%      { opacity: 0.85; transform: scale(1.3); }
    }
  </style>
  <div class="root">
    
    <span class="corner tl" id="cta-corner-tl"
          data-anim="fade-in" data-anim-at="0.20" data-anim-duration="0.50"></span>
    <span class="corner tr" id="cta-corner-tr"
          data-anim="fade-in" data-anim-at="0.20" data-anim-duration="0.50"></span>
    <span class="corner bl" id="cta-corner-bl"
          data-anim="fade-in" data-anim-at="0.20" data-anim-duration="0.50"></span>
    <span class="corner br" id="cta-corner-br"
          data-anim="fade-in" data-anim-at="0.20" data-anim-duration="0.50"></span>

    
    <div class="top-meta" id="cta-top-meta"
         data-anim="fade-in" data-anim-at="0.25" data-anim-duration="0.50">
      <span>VTAKE&trade;</span>
      <span class="sep">&diams;</span>
      <span>Cinematic Postproduction</span>
      <span class="sep">&diams;</span>
      <span>Est. 2026</span>
    </div>

    
    <div class="bot-meta" id="cta-bot-meta"
         data-anim="fade-in" data-anim-at="0.30" data-anim-duration="0.50">
      <span>&copy; 2026 vTake Studio</span>
      <span class="right">vtake.app</span>
    </div>

    
    <span class="spark s1"></span>
    <span class="spark s2"></span>
    <span class="spark s3"></span>
    <span class="spark s4"></span>
    <span class="spark s5"></span>

    <div class="stage">
      
      <div class="powered-row" id="cta-powered-row">
        <span class="line" id="cta-line-l"
              data-anim="grow-x" data-anim-at="0.18"
              data-anim-duration="0.40" data-anim-target-w="56"></span>
        <span class="label" id="cta-powered-label"
              data-anim="fade-in" data-anim-at="0.15"
              data-anim-duration="0.45">Powered by</span>
        <span class="line" id="cta-line-r"
              data-anim="grow-x" data-anim-at="0.18"
              data-anim-duration="0.40" data-anim-target-w="56"></span>
      </div>

      
      <div class="mark-wrap">
        <span class="mark-ring" id="cta-ring-outer"
              data-anim="fade-in" data-anim-at="0.55" data-anim-duration="0.50"></span>
        <span class="mark-ring inner" id="cta-ring-inner"
              data-anim="fade-in" data-anim-at="0.60" data-anim-duration="0.50"></span>
        <svg class="vtake-mark" viewBox="0 0 200 130" fill="none"
             aria-label="vTake" role="img">
          <g transform="skewX(-14) translate(16 0)"
             stroke="currentColor" stroke-width="14"
             stroke-linecap="round" stroke-linejoin="round">
            <path id="cta-path-v"     d="M24 32 L56 100 L88 32"
                  data-anim="draw-path" data-anim-at="0.05" data-anim-duration="0.40"/>
            <path id="cta-path-tbar"  d="M100 32 L164 32"
                  data-anim="draw-path" data-anim-at="0.30" data-anim-duration="0.25"/>
            <path id="cta-path-tstem" d="M132 32 L132 100"
                  data-anim="draw-path" data-anim-at="0.50" data-anim-duration="0.25"/>
          </g>
          <circle id="cta-dot" cx="48" cy="118" r="7.2" fill="#E89034"
                  data-anim="scale-pop" data-anim-at="0.65" data-anim-duration="0.25"/>
        </svg>
      </div>

      
      <div id="cta-vtake-name" class="vtake-name"
           data-anim="mask-reveal" data-anim-at="0.40"
           data-anim-duration="0.45" data-anim-direction="bottom">
        <span class="name-accent">v</span>Take
      </div>

      
      <div class="divider">
        <span class="seg" id="cta-seg-l"
              data-anim="grow-x" data-anim-at="0.75"
              data-anim-duration="0.30" data-anim-target-w="56"></span>
        <span class="diamond" id="cta-diamond"
              data-anim="scale-pop" data-anim-at="0.85" data-anim-duration="0.30"></span>
        <span class="seg" id="cta-seg-r"
              data-anim="grow-x" data-anim-at="0.75"
              data-anim-duration="0.30" data-anim-target-w="56"></span>
      </div>

      
      <div class="tagline" id="cta-tagline"
           data-anim="fade-in" data-anim-at="0.90" data-anim-duration="0.45">
        Visual Take &middot; Reimagined
      </div>
    </div>
  </div>
</div>

アニメーションのタイムライン(登場は1.00s、すべてのdata-anim-at値は card-cta-vtake.startSecを基準とします):

At要素アニメーションDur
0.05V字型のストロークdraw-path0.40
0.15「Powered by」ラベルfade-in0.45
0.18両側のラインgrow-x(→56px)0.40
0.20ビューファインダーのコーナー ×4fade-in0.50
0.25上部のフィルムクレジットストリップfade-in0.50
0.30下部のフィルムクレジットストリップfade-in0.50
0.30Tバーのストロークdraw-path0.25
0.40「vTake」のメインネームmask-reveal0.45
0.50T字の縦線のストロークdraw-path0.25
0.55外側の破線リングfade-in(+ 32s回転)0.50
0.60内側の実線リングfade-in(+ 32s逆回転)0.50
0.65アンバー色のドットscale-pop(弾性)0.25
0.75区切り線のセグメントgrow-x(→56px)0.30
0.85区切り線のダイヤモンドscale-pop0.30
0.90タグラインfade-in0.45
1.00+環境アニメーションリングの回転 + きらめきのループ—
9. コンポジションHTMLを組み立てる

アセットを配置し、$WORK_DIR/public/index.htmlを作成します。

# SKILL_DIR is injected by the host ("Base directory for this skill: …")
SKILL_DIR="<SKILL_DIR>"

mkdir -p "$WORK_DIR/public/fonts" "$WORK_DIR/public/vendor" "$WORK_DIR/public/cards"
cp -n "$SKILL_DIR/assets/fonts/"*            "$WORK_DIR/public/fonts/"
cp -n "$SKILL_DIR/assets/vendor/gsap.min.js" "$WORK_DIR/public/vendor/"
# stage the input video so the composition can reference it by relative path
ln -f "$VIDEO_PATH" "$WORK_DIR/public/input-video.mp4" 2>/dev/null \
  || cp "$VIDEO_PATH" "$WORK_DIR/public/input-video.mp4"
コンポジションテンプレート
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<style>
@font-face { font-family: 'Caveat'; src: url('fonts/Caveat-400-latin.woff2') format('woff2'); font-weight: 400; font-display: block; }
@font-face { font-family: 'Caveat'; src: url('fonts/Caveat-700-latin.woff2') format('woff2'); font-weight: 700; font-display: block; }
@font-face { font-family: 'LXGW WenKai TC'; src: url('fonts/LXGWWenKaiTC-400-latin.woff2') format('woff2'); font-weight: 400; font-display: block; }
@font-face { font-family: 'Inter'; src: url('fonts/Inter-400-latin.woff2') format('woff2'); font-weight: 400; font-display: block; }
@font-face { font-family: 'Inter'; src: url('fonts/Inter-700-latin.woff2') format('woff2'); font-weight: 700; font-display: block; }
@font-face { font-family: 'Virgil'; src: url('fonts/Virgil.woff2') format('woff2'); font-display: block; }

:root {
  /* Pick from the themeId palette table in Step 7 — example: classic */
  --bg: #FFF9E3;
  --text: #1e1e1e;
  --accent-0: #1971c2;
  --accent-1: #e03131;
  --accent-2: #2f9e44;
  --accent-3: #e8590c;
  --accent-4: #9c36b5;
  --font-family: 'Caveat', 'LXGW WenKai TC', serif;
}
* { box-sizing: border-box; }
/* Body font-family MUST list concrete font names (not just var(--font-family)) —
   the HyperFrames renderer's static analyzer doesn't expand CSS variables when
   resolving fonts, so a var-only chain triggers `font_family_without_font_face`
   lint and falls back to a generic. Use the concrete chain here; cards that
   want the theme font can still reference var(--font-family) internally. */
html, body { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; background: #000;
             font-family: 'Inter', 'Caveat', 'LXGW WenKai TC', ui-sans-serif, system-ui, sans-serif; }
#stage { position: relative; width: 100%; height: 100%; overflow: hidden; }

/* video-wrapper holds the source video. Its position / size are animated
   over time by the master timeline (one tween per layout transition). */
.video-wrapper {
  position: absolute;
  left: 0; top: 0; width: 1920px; height: 1080px;
  overflow: hidden;
  border-radius: 0;
  box-shadow: none;
}
.video-wrapper video { width: 100%; height: 100%; object-fit: cover; }

.card-host { position: absolute; pointer-events: none; overflow: hidden; }
.card-host .card { position: relative; width: 100%; height: 100%; overflow: hidden; }
.card-host .char { display: inline-block; visibility: visible; }

/* Subtle drop shadow + rounded corners for non-fullscreen video framings */
.video-wrapper.framed {
  border-radius: 16px;
  box-shadow: 0 12px 40px rgba(0,0,0,0.35);
}
</style>
</head>
<body>
<div
  id="stage"
  data-composition-id="vtake"
  data-start="0"
  data-duration="121.2"
  data-fps="30"
  data-width="1920"
  data-height="1080"
>
  
  <div class="video-wrapper" id="video-wrap">
    <video id="bg-video"
           src="input-video.mp4"
           muted playsinline
           data-start="0"
           data-duration="121.2"
           data-track-index="1"></video>
  </div>

  
  
  
  
  
  
  
  
  <div class="card-host clip"
       data-card-id="card-01"
       data-start="1.0000"
       data-duration="6.5000"
       data-track-index="2"
       style="left:0;top:0;width:1920px;height:1080px;visibility:hidden;opacity:0;">
    
  </div>

  
  <div class="card-host clip"
       data-card-id="card-02"
       data-start="8.0000"
       data-duration="12.0000"
       data-track-index="2"
       style="left:0;top:0;width:960px;height:1080px;visibility:hidden;opacity:0;">
    
  </div>

  

  <script src="vendor/gsap.min.js"></script>
  <script>
  (function(){
    // count-up formatter helper
    window.__fmt = function(v, fmt) {
      if (typeof fmt === 'string' && /^\.[0-9]+f$/.test(fmt)) {
        return Number(v).toFixed(Number(fmt.slice(1, -1)));
      }
      if (fmt === ',d') return Math.round(v).toLocaleString();
      return String(Math.round(v));
    };

    const tl = window.gsap.timeline({ paused: true });

    // ── Card lifecycle (one block per card) ──
    // Example for card-01 [1.0, 7.5] with kinetic-chars at +0.3, grow-x at +0.65:

    // Enter (fade in over 0.4s)
    tl.set('.card-host[data-card-id="card-01"]',  { visibility: 'visible' }, 1.0000);
    tl.fromTo('.card-host[data-card-id="card-01"]',
              { opacity: 0 }, { opacity: 1, duration: 0.4000, ease: 'power2.out' }, 1.0000);

    // Card-internal anims (compile each data-anim-* declaration here)
    tl.from('.card[data-card-id="card-01"] #card-01-title .char',
            { opacity: 0, y: 8, scale: 0.8, duration: 0.5000, ease: 'power2.out', stagger: 0.0400 },
            1.3000);
    tl.fromTo('.card[data-card-id="card-01"] #card-01-line',
              { width: 0 }, { width: 420, duration: 0.5000, ease: 'power2.out' }, 1.6500);

    // Exit (fade out over 0.35s, ending at endSec)
    tl.to('.card-host[data-card-id="card-01"]',
          { opacity: 0, duration: 0.3500, ease: 'power2.in' }, 7.1500);
    tl.set('.card-host[data-card-id="card-01"]', { visibility: 'hidden' }, 7.5000);

    // ── Video framing transitions ──
    // When the next card uses a different composition layout, animate the
    // video-wrapper to its new bounds. Example: card-01 = fullscreen
    // (video hidden behind), card-02 = split composition (zone="side-panel"
    // → video on right, card on left).

    // Card-02 enters at 8.0s with the split composition. Animate video to
    // the right half during the card-01 → card-02 gap (between 7.5 and 8.0s).
    tl.set('#video-wrap', { className: 'video-wrapper framed' }, 7.5);
    tl.to('#video-wrap',
          { left: 960, top: 0, width: 960, height: 1080,
            duration: 0.6, ease: 'power2.inOut' }, 7.5);

    // Card-02 enter — same pattern as card-01
    tl.set('.card-host[data-card-id="card-02"]', { visibility: 'visible' }, 8.0);
    tl.fromTo('.card-host[data-card-id="card-02"]',
              { opacity: 0 }, { opacity: 1, duration: 0.4, ease: 'power2.out' }, 8.0);
    // ...card-02 internal anims...

    // ── repeat for each card; if the NEXT card's layout differs,
    //    insert another tl.to('#video-wrap', ...) tween before its enter ──

    window.__timelines = window.__timelines || {};
    window.__timelines["vtake"] = tl;
  })();
  </script>
</div>
</body>
</html>
GSAPステートメント早見表

各data-anim属性をGSAPステートメントに変換します。時刻は 絶対秒数=card.startSec + data-anim-atで、1/fps単位に量子化します。 セレクターは.card[data-card-id="X"] #elementIdです。

data-animGSAPステートメントのテンプレート
fade-intl.fromTo(SEL, { opacity: 0 }, { opacity: 1, duration: D, ease: 'power2.out' }, T);
fade-outtl.to(SEL, { opacity: 0, duration: D, ease: 'power2.in' }, T);
slide-in (from=left, dist=80)tl.fromTo(SEL, { opacity: 0, x: -80 }, { opacity: 1, x: 0, duration: D, ease: 'power2.out' }, T);
kinetic-chars (pop)tl.from(SEL + ' .char', { opacity: 0, y: 8, scale: 0.8, duration: D, ease: 'power2.out', stagger: S }, T);
count-up(function(){const o={v:FROM};tl.to(o,{v:TO,duration:D,ease:'power2.out',onUpdate:function(){const el=document.querySelector(SEL);if(el)el.textContent=__fmt(o.v,'FMT');}},T);})();
draw-path(function(){const el=document.querySelector(SEL);if(!el)return;const L=el.getTotalLength();tl.set(SEL,{strokeDasharray:L,strokeDashoffset:L},T);tl.to(SEL,{strokeDashoffset:0,duration:D,ease:'power2.inOut'},T);})();
grow-x (target-w=W)tl.fromTo(SEL, { width: 0 }, { width: W, duration: D, ease: 'power2.out' }, T);
grow-y (target-h=H)tl.fromTo(SEL, { height: 0 }, { height: H, duration: D, ease: 'power2.out' }, T);
scale-poptl.fromTo(SEL, { opacity: 0, scale: 0.6 }, { opacity: 1, scale: 1, duration: D, ease: 'back.out(1.6)' }, T);
mask-reveal (direction=left)tl.fromTo(SEL, { clipPath: 'inset(0 100% 0 0)' }, { clipPath: 'inset(0 0 0 0)', duration: D, ease: 'power2.inOut' }, T);

量子化:T = Math.round(absSec * fps) / fps。30fpsでは最小ステップは 1/30 ≈ 0.0333sです。JSリテラル内では小数点以下4桁に丸める(.toFixed(4))方法で問題ありません。

動画のフレーミングリファレンス(layout の値ごと)

動画コンテナーのセレクターは #video-wrap です。カード間で境界をアニメーションさせるには、tl.to('#video-wrap', { ...bounds }, T) を使用します。初期の境界は、card-01 のレイアウトに合わせて要素にインラインで設定してください。トランジションの長さは 0.5~0.7s とし、ease: 'power2.inOut' を指定します。

装飾フレーム(clean/hairline/polaroid)は #video-wrap の兄弟要素として配置し、レイアウトのトランジション中も追従させます。各フレームの配置用 HTML、推奨 CSS、対応するレイアウトについては、references/frames/ を参照してください。簡単なルール:overlay レイアウトでは装飾フレームを表示しません(フルブリード動画と装飾が干渉するため)。PiP レイアウトには独自のピル状スタイル(border-radius + 白いリング + 影)がすでにあるため、装飾フレームは split/stack の上にのみ追加します。

#video-wrap の GSAP ターゲット参照表(コンポジションの各レイアウトに対応) (横長 1920×1080 — 縦長と 4:5 については、3 つの比率すべてを記載した references/layouts/*.html を参照してください):

コンポジションのレイアウト典型的な card.zone#video-wrap の GSAP ターゲット追加の css クラス
splitside-panel{ left: 960, top: 0, width: 960, height: 1080 }—
stacklower-third{ left: 14, top: 14, width: 1892, height: 548 }(上から 52%)—
pip(右下)fullscreen{ left: 1480, top: 760, width: 400, height: 300 }pip-pill(border-radius + ring + shadow)
pip(左上)fullscreen{ left: 40, top: 40, width: 400, height: 300 }pip-pill
overlay(動画が全面表示)video-overlay{ left: 0, top: 0, width: 1920, height: 1080 }(デフォルトから変更なし)—
動画を非表示(純粋なグラフィックの場面)fullscreen{ opacity: 0 }(またはキャンバスの外に移動)—

PiP の場面に入るとき、または PiP の場面から出るときに、pip-pill の装飾(border-radius + 白いリング + ドロップシャドウ)を切り替えるには:

// Enter pip — add chrome
tl.set('#video-wrap', { className: 'video-wrapper pip-pill' }, T);
tl.to('#video-wrap', { left: 1480, top: 760, width: 400, height: 300,
                       duration: 0.6, ease: 'power2.inOut' }, T);

// Leave pip — back to clean full-bleed
tl.set('#video-wrap', { className: 'video-wrapper' }, T_NEXT);
tl.to('#video-wrap', { left: 0, top: 0, width: 1920, height: 1080,
                       duration: 0.6, ease: 'power2.inOut' }, T_NEXT);

カードホストの境界はゾーンに合わせます。Step 6 の冒頭にある表を使って、カードの zone をピクセル単位の境界に変換し、その値をカードホストのインライン style="left:Xpx;top:Ypx;width:Wpx; height:Hpx;..." に記述します。video-overlay ゾーン(overlay レシピ)の場合、カードホストはキャンバス全体を埋めます。実際に見えるカードをどこに配置するかは、.card .root 内の CSS で決まります。

HyperFrames のレイアウト/アニメーション QA ルール
  • 各カードの静止ヒーローフレーム(カード全体が表示され、読みやすい状態)を最初に作成します。
  • 動画、カード、字幕/キャプション、図が意図せず重なっていないことを確認します。
  • 非表示の動画領域がフレームでクリップされ、意図した境界の外に表示されていないことを確認します。
  • 一時停止状態のマスタータイムラインを window.__timelines["vtake"] として登録します。
  • ページ読み込み時にタイムラインを同期的に作成します。async、setTimeout、Promises、メディアの play() 呼び出しは使用しないでください。
  • レンダリング処理内で Math.random() や Date.now() を使用しないでください。
  • repeat: -1 は使用せず、動画の長さをもとに有限回の繰り返し数を計算してください。
  • モーションには、レイアウトプロパティ(top、left、width、height)よりも、GSAP のトランスフォームと不透明度(x、y、scale、rotation、opacity)を優先してください。
  • 動画要素のサイズを直接アニメーションするのではなく、#video-wrap などのラッパーをアニメーションしてください。
  • 同じ要素の同じプロパティを、複数のタイムラインで同時にアニメーションしないでください。
  • data-layer ではなく data-track-index を、data-end ではなく data-duration を使用してください。
  • すべての時間指定要素(card-host、サブコンポジションなど)には、独自のクラスとともに class="clip" を必ず含めてください。例:class="card-host clip"。HyperFrames ランタイムは .clip を使用して、data-start … data-start+data-duration の範囲内でのみ要素を表示します。これがない場合、要素は動画全体に表示されます(lint:timed_element_missing_clip_class)。
  • body/グローバルの font-family には、'Inter', 'Caveat', … のような具体的なフォント名を指定してください。var(--font-family) のような CSS 変数は使用しないでください。HyperFrames のフォントリゾルバーは静的解析時に CSS 変数を展開しません(lint:font_family_without_font_face)。カードでは @font-face 宣言が読み込まれるため、内部で var(--font-family) を使用できます。
card-cta-vtake:固定 GSAP アニメーションブロック

GSAP タイムラインブロックの末尾(最後の window.__timelines 登録の直前)に、この固定コードブロックを追加してください。CTA_START を card-cta-vtake.startSec に、CTA_END を card-cta-vtake.endSec に置き換えてください。

// ── card-cta-vtake: Editorial Cinema brand outro (2.0s total) ──
// Sequence: 1.0s entrance · 0.7s hold (CSS ambient breathing) · 0.3s fade-out

const PREFIX = '.card[data-card-id="card-cta-vtake"]';

// Fade source video out at the start of the CTA
tl.to('#video-wrap', { opacity: 0, duration: 0.30, ease: 'power2.in' }, CTA_START);

// Card host enter
tl.set('.card-host[data-card-id="card-cta-vtake"]', { visibility: 'visible' }, CTA_START);
tl.fromTo('.card-host[data-card-id="card-cta-vtake"]',
          { opacity: 0 }, { opacity: 1, duration: 0.30, ease: 'power2.out' }, CTA_START);

// V-shape stroke draw
(function(){
  const sel = PREFIX + ' #cta-path-v';
  const el = document.querySelector(sel); if(!el) return;
  const L = el.getTotalLength();
  tl.set(sel, { strokeDasharray: L, strokeDashoffset: L }, CTA_START + 0.05);
  tl.to(sel, { strokeDashoffset: 0, duration: 0.40, ease: 'power2.inOut' }, CTA_START + 0.05);
})();

// "Powered by" label
tl.fromTo(PREFIX + ' #cta-powered-label',
          { opacity: 0 }, { opacity: 1, duration: 0.45, ease: 'power2.out' }, CTA_START + 0.15);

// Flanking lines grow-x
tl.fromTo(PREFIX + ' #cta-line-l',
          { width: 0 }, { width: 56, duration: 0.40, ease: 'power2.out' }, CTA_START + 0.18);
tl.fromTo(PREFIX + ' #cta-line-r',
          { width: 0 }, { width: 56, duration: 0.40, ease: 'power2.out' }, CTA_START + 0.18);

// Viewfinder corners ×4 fade-in
['tl','tr','bl','br'].forEach(pos => {
  tl.fromTo(PREFIX + ' #cta-corner-' + pos,
            { opacity: 0, scale: 0.7 },
            { opacity: 1, scale: 1, duration: 0.50, ease: 'power2.out' },
            CTA_START + 0.20);
});

// Top/bottom film-credit meta strips
tl.fromTo(PREFIX + ' #cta-top-meta',
          { opacity: 0, y: 6 }, { opacity: 1, y: 0, duration: 0.50, ease: 'power2.out' }, CTA_START + 0.25);
tl.fromTo(PREFIX + ' #cta-bot-meta',
          { opacity: 0, y: -6 }, { opacity: 1, y: 0, duration: 0.50, ease: 'power2.out' }, CTA_START + 0.30);

// T-bar stroke draw
(function(){
  const sel = PREFIX + ' #cta-path-tbar';
  const el = document.querySelector(sel); if(!el) return;
  const L = el.getTotalLength();
  tl.set(sel, { strokeDasharray: L, strokeDashoffset: L }, CTA_START + 0.30);
  tl.to(sel, { strokeDashoffset: 0, duration: 0.25, ease: 'power2.inOut' }, CTA_START + 0.30);
})();

// "vTake" main name mask-reveal (left → right)
tl.fromTo(PREFIX + ' #cta-vtake-name',
          { clipPath: 'inset(0 100% 0 0)' },
          { clipPath: 'inset(0 0% 0 0)', duration: 0.45, ease: 'power2.inOut' },
          CTA_START + 0.40);

// T-stem stroke draw
(function(){
  const sel = PREFIX + ' #cta-path-tstem';
  const el = document.querySelector(sel); if(!el) return;
  const L = el.getTotalLength();
  tl.set(sel, { strokeDasharray: L, strokeDashoffset: L }, CTA_START + 0.50);
  tl.to(sel, { strokeDashoffset: 0, duration: 0.25, ease: 'power2.inOut' }, CTA_START + 0.50);
})();

// Dual concentric rings fade-in (CSS @keyframes handles the slow spin)
tl.fromTo(PREFIX + ' #cta-ring-outer',
          { opacity: 0 }, { opacity: 1, duration: 0.50, ease: 'power2.out' }, CTA_START + 0.55);
tl.fromTo(PREFIX + ' #cta-ring-inner',
          { opacity: 0 }, { opacity: 1, duration: 0.50, ease: 'power2.out' }, CTA_START + 0.60);

// Amber dot scale-pop (elastic)
tl.fromTo(PREFIX + ' #cta-dot',
          { opacity: 0, scale: 0.4, transformOrigin: '48px 118px' },
          { opacity: 1, scale: 1, duration: 0.25, ease: 'back.out(1.7)' },
          CTA_START + 0.65);

// Decorative divider segments grow-x
tl.fromTo(PREFIX + ' #cta-seg-l',
          { width: 0 }, { width: 56, duration: 0.30, ease: 'power2.out' }, CTA_START + 0.75);
tl.fromTo(PREFIX + ' #cta-seg-r',
          { width: 0 }, { width: 56, duration: 0.30, ease: 'power2.out' }, CTA_START + 0.75);

// Divider diamond pop
tl.fromTo(PREFIX + ' #cta-diamond',
          { rotate: 45, scale: 0 },
          { rotate: 45, scale: 1, duration: 0.30, ease: 'back.out(1.7)' },
          CTA_START + 0.85);

// Tagline fade-in
tl.fromTo(PREFIX + ' #cta-tagline',
          { opacity: 0, y: 4 }, { opacity: 1, y: 0, duration: 0.45, ease: 'power2.out' }, CTA_START + 0.90);

// Card exit — fade-out in the last 0.30s of the 2.0s window
tl.to('.card-host[data-card-id="card-cta-vtake"]',
      { opacity: 0, duration: 0.30, ease: 'power2.in' }, CTA_END - 0.30);
tl.set('.card-host[data-card-id="card-cta-vtake"]', { visibility: 'hidden' }, CTA_END);
10. MP4 にレンダリングする
cd "$WORK_DIR"
PRODUCER_BROWSER_GPU_MODE=hardware npx hyperframes render public \
  -o output.mp4 \
  --fps 30

hyperframes render <dir> は <dir>/index.html を読み込み、MP4 を生成します。 macOS では PRODUCER_BROWSER_GPU_MODE=hardware(または --browser-gpu)フラグの使用を強くおすすめします。ソフトウェアのみでの Chrome レンダリングは、ほとんどのノートパソコンでタイムアウトします。

本番のレンダリング前に動作を確認するため、特定のタイムスタンプのフレームを 1 枚キャプチャします。

npx hyperframes snapshot public --at 5 --out snapshot-5s.png
11. 結果を報告する

ユーザーに以下を伝えてください。

  • 作業ディレクトリのパス
  • storyboard.json(作成したカードのアウトライン)
  • public/cards/*.html(カードごとに 1 つの HTML ファイル)
  • public/index.html(組み立てたコンポジション)
  • output.mp4(最終動画)
  • 使用した ASR プロバイダー
  • カード数と選定方法(1 文で)
  • 不足しているキーや品質に関する注意事項

ユーザーから依頼がない限り、作業ディレクトリを削除しないでください。

コメント