Communityコーディング&開発github.com

dev-claude-reel

約50秒の縦型(1080x1920)3Dリールを作成し、開発者と「Claude」キャラクターが短く面白い会話をします。スクリプト、ElevenLabsの音声、フレームごとにヘッドレスChromeでレンダリングするthree.jsシーン、カメラワークとカット、オーディオミックス、最終的なmp4、さらにフルレンダリング前に不具合のある動きをキャッチするQAパスを処理します。

dev-claude-reel とは?

これは、開発者とClaudeキャラクターが登場する約50秒の縦型3Dアニメーションリールを自動生成するAIエージェントスキルです。スクリプト作成、ElevenLabsを使用した音声生成、three.jsによるフレームごとのレンダリング、オーディオミキシング、動画ファイルの出力までをパイプライン形式で処理します。主にClaude CodeなどのAIエージェントで使用することを想定しており、Node.js、ヘッドレスChrome、ffmpegを必要とします。重要なのは、完全レンダリングの前に静止画チェックを行うことで、高コストな再レンダリングを防ぐ点です。すべての処理はユーザーの日常的なコンピュータ上で低優先度で実行され、GPUがソフトウェアレンダラーの場合は中断します。ElevenLabsのAPIキーは環境変数から安全に読み取られ、スクリプトと音声はキャッシュされるため、同じコンテンツに二重にコストがかかることはありません。

対応✓Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/NiklasMVP/dev-claude-reel/tree/HEAD/dev-claude-reel

Installed? Explore more コーディング&開発 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

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

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

ドキュメント

dev-claude-reel は何をしますか?

出力: 1つのmp4、1080x1920、30 fps、H.264 + AAC、約50秒。低ポリゴンの3Dシーン、2人のキャラクター(開発者とClaudeキャラクター)、実際の音声、焼き込み字幕、会話に合わせてカットされるカメラ、そしてクリーンなミックス。

方法: 単一のsetT(t)関数によって駆動されるthree.jsページ。ヘッドレスChromeがフレームごとにレンダリングし、ffmpegがフレームとオーディオを結合します。

このスキルが決定しないこと: キャラクターの見た目、場所、会話内容。ユーザーが指定した場合はそれを使用します。そうでなければ、数行で何かを選んで進めてください。

パイプライン(この順序で、スキップ不可): 概要 → スクリプト → 音声 → タイムライン → シーン → 静止画 → モーションチェック → ミックス → フルレンダリング → マルチプレックス → 検証 → 配信


1. 基本ルール

これらのルールは利便性よりも優先されます。ユーザーの指示には優先しません。

  • マシンに負荷をかけないこと。 ユーザーの日常的なコンピュータです。1つのヘッドレスブラウザ、GPUレンダリング、最低プロセス優先度、並列ワーカーなし。WebGLがソフトウェアレンダラー(SwiftShader / "software" / "Basic Render")を報告した場合、停止してユーザーに通知してください。ソフトウェアGLを強制しないでください。
  • シークレットは秘密のままに。 ElevenLabsキーはELEVENLABS_API_KEY(環境変数または.env)に保存します。スクリプト内でのみ読み取ってください。決して表示、ログ出力、または可視化される場所に書き込まないでください。
  • まず静止画、次に動画。 静止画のコンタクトシートが適切に見えるまでフルレンダリングは行わないでください。ワンショットのミスでフルレンダリングをやり直すことは、このワークフローで最もコストがかかるエラーです。
  • フォアグラウンドチャンクでレンダリング。 バックグラウンドジョブはターン終了時に強制終了されます。各レンダリング呼び出しは約8分未満に保ち、再開可能にしてください(セクション11)。
  • チャットごとに1つの動画。 シーンファイルは急速に大きくなります。パーツに分割し、不必要に再読み込みせず、次のリールのために新しいチャットを開始してください。
  • ファイルを配信し、決して公開しないこと。 公開はユーザーの判断です。
  • ギャップについて正直に。 キー、ffmpeg、またはChromeがない場合?何が欠けているかを正確に伝え、最も近い実行可能なオプションを提供してください。結果を偽ってはいけません。

2. セットアップチェック(最初の1分)

必要項目備考
Node 22+組み込みのfetchとWebSocketを使用。npm依存関係はthree.jsのみ。
Chrome または Edgeヘッドレスで実行。
ffmpeg + ffprobePATH上か既知の場所に。
three.jsnpm i [email protected]、ローカル静的サーバーからディスク上で提供。レンダリング中はCDN、ネットワークは不要。
ElevenLabsキー無料枠でも動作(約10,000クレジット/月、APIアクセス、既成の音声、商用ライセンスなし)。約1クレジット/文字、サウンド生成は別途コストがかかります。
GPU一度プローブ(セクション3)。

ElevenLabs予算: 生成する前にスクリプトを完成させ、その文字数をカウントし、重要なセリフのみ代替テイクを作成し、すべての結果をキャッシュして、同じものに二重に支払いが発生しないようにします。

モデル: eleven_v3。アカウントが使用できない場合は、eleven_multilingual_v2にフォールバックし、テキストから[audio tags]を削除し、演技がより平板になることをユーザーに警告します。

キーがない場合? その旨を伝えます。スクリプト、シーン、および無音のテストレンダリングを役立つ範囲で構築し、キーを要求します。ユーザーがキーなしで進めることを主張する場合:OSのテキスト読み上げ、文字数から推定されたワードタイミング。口パクと字幕がより粗くなることを伝えてください。

フォルダ構成

動画ごとに1つのフォルダ、プレーンなファイル、ビルドステップなし。

reel/
  package.json      three
  film.html         キャンバス + 字幕オーバーレイ + import map、scene.jsを読み込み
  scene.js          3Dワールド + setT(t)(大きくなったらパーツに分割)
  timeline.mjs      タイミング + ショット、Nodeとブラウザの両方でインポート
  gen-audio.mjs     takes.json -> ElevenLabs音声 + アンビエンス(ディスクにキャッシュ)
  words.mjs         アライメントjsonからのワードタイミング
  build-film.mjs    スピーカーごとの音声バス + film.json(エンベロープ、字幕)
  music.mjs         オプションのBGM
  mix.mjs           最終ミックス
  cdp.mjs           ヘッドレスChromeドライバー
  render.mjs        静止画 | フル(チャンク分割、再開可能)
  check.mjs         自動QA
  make.mjs          gen-audio -> build-film -> music -> mix を実行
  vo/ sfx/ frames/ stills/

timeline.mjsへの変更は、make.mjsを最初から最後まで再実行することを意味します。 古いオーディオステムは典型的な目に見えないバグです。古いタイミングに属するサウンドがランダムなクリックとして現れます。


3. ヘッドレスChromeの制御(cdp.mjs)

依存関係のない小さなChrome DevTools Protocolクライアント。

起動フラグ: --headless=new --remote-debugging-port=<p> --user-data-dir=<tmp> --no-first-run --disable-extensions --disable-background-timer-throttling --disable-renderer-backgrounding --mute-audio --window-size=W,H --use-gl=angle --use-angle=<backend> --ignore-gpu-blocklist --enable-gpu バックエンド: Windowsではd3d11、macOSではmetal、Linuxではgl。

接続後:

  • Emulation.setDeviceMetricsOverride { width: 1080, height: 1920, deviceScaleFactor: 1, mobile: false }
  • GPUプローブ: WEBGL_debug_renderer_infoを介してUNMASKED_RENDERER_WEBGLを読み取ります。空、または /swiftshader|software|basic render/i に一致する場合 → 中断してユーザーに通知します。
  • 丁寧に振る舞う: すべてのNodeスクリプトの先頭で os.setPriority(os.constants.priority.PRIORITY_LOW)。約5秒ごとに、ブラウザとすべての子プロセスの優先度を再度下げます(ChromeはGPUプロセスを元に戻すため)。子プロセスPIDは、ブラウザのwebsocket(/json/version → webSocketDebuggerUrl)で SystemInfo.getProcessInfo から取得します。Windowsでは、PowerShellを介してブラウザを1つの論理コアに固定します((Get-Process -Id <pid>).ProcessorAffinity = <single bit>)。

APIサーフェス: goto(url)、eval(expr)(awaitPromise + returnByValue、exceptionDetailsでスロー)、waitFor(expr, timeout)、send(method, params)、logs配列(コンソール呼び出し、例外、ログエントリ)、close()(プロセスを強制終了、一時プロファイルを削除)。

開始点(適応させて使用し、盲目的に貼り付けないでください):

import { spawn } from 'node:child_process';
import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path';

const BROWSERS = [
  'C:/Program Files/Google/Chrome/Application/chrome.exe',
  'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe',
  '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
  '/usr/bin/google-chrome',
];
const BIN = BROWSERS.find(p => fs.existsSync(p));
const ANGLE = { win32: 'd3d11', darwin: 'metal' }[process.platform] ?? 'gl';
const sleep = ms => new Promise(r => setTimeout(r, ms));

export async function launch({ width = 1080, height = 1920, port = 9450 } = {}) {
  try { os.setPriority(os.constants.priority.PRIORITY_LOW); } catch {}
  const profile = fs.mkdtempSync(path.join(os.tmpdir(), 'reel-'));
  const proc = spawn(BIN, [
    `--remote-debugging-port=${port}`, `--user-data-dir=${profile}`, '--headless=new',
    '--no-first-run', '--disable-extensions', '--disable-background-timer-throttling',
    '--disable-renderer-backgrounding', '--mute-audio', `--window-size=${width},${height}`,
    '--use-gl=angle', `--use-angle=${ANGLE}`, '--ignore-gpu-blocklist', '--enable-gpu', 'about:blank',
  ], { stdio: 'ignore' });

  let targets;
  for (let i = 0; i < 100 && !targets?.length; i++) {
    try { targets = await (await fetch(`http://127.0.0.1:${port}/json/list`)).json(); }
    catch { await sleep(150); }
  }
  const ws = new WebSocket(targets.find(t => t.type === 'page').webSocketDebuggerUrl);
  await new Promise((ok, fail) => { ws.onopen = ok; ws.onerror = fail; });

  let seq = 0; const waiting = new Map(); const logs = [];
  ws.onmessage = ({ data }) => {
    const msg = JSON.parse(data);
    if (msg.id && waiting.has(msg.id)) {
      const { ok, fail } = waiting.get(msg.id); waiting.delete(msg.id);
      msg.error ? fail(new Error(msg.error.message)) : ok(msg.result);
    } else if (msg.method === 'Runtime.exceptionThrown') {
      const d = msg.params.exceptionDetails;
      logs.push({ type: 'exception', text: d.exception?.description || d.text });
    } else if (msg.method === 'Runtime.consoleAPICalled') {
      logs.push({ type: msg.params.type, text: msg.params.args.map(a => a.value ?? a.description).join(' ') });
    }
  };
  const send = (method, params = {}) => new Promise((ok, fail) => {
    const id = ++seq; waiting.set(id, { ok, fail });
    ws.send(JSON.stringify({ id, method, params }));
  });

  await send('Runtime.enable'); await send('Page.enable'); await send('Log.enable');
  await send('Emulation.setDeviceMetricsOverride', { width, height, deviceScaleFactor: 1, mobile: false });
  // TODO: GPUプローブ、proc.pid + 子プロセスの5秒優先度低下タイマー、Windowsアフィニティ固定

  const api = {
    send, logs, proc,
    async eval(expression) {
      const r = await send('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true });
      if (r.exceptionDetails) throw new Error('eval failed: ' + (r.exceptionDetails.exception?.description || r.exceptionDetails.text));
      return r.result.value;
    },
    async goto(url) { await send('Page.navigate', { url }); await api.waitFor('document.readyState === "complete"', 15000); },
    async waitFor(expr, ms = 10000) {
      for (const t0 = Date.now(); Date.now() - t0 < ms; await sleep(100)) {
        try { if (await api.eval(expr)) return true; } catch {}
      }
      throw new Error('timeout: ' + expr);
    },
    async close() {
      try { ws.close(); } catch {} try { proc.kill(); } catch {}
      setTimeout(() => fs.rmSync(profile, { recursive: true, force: true }), 500);
    },
  };
  return api;
}

4. フィルムページ(film.html + scene.js)

黄金律

window.setT(t)は時間の純粋関数です。 秒数を与えると、すべてのキャラクター、プロップ、ライト、カメラのポーズを取り、フレームをレンダリングして完了します。つまり:

  • performance.now()、requestAnimationFrame、蓄積された状態、物理演算は不要
  • Math.random()は使用しない:代わりにインデックスのシード付きハッシュを使用

このため、フレームは任意の順序でレンダリングでき、チャンクはクリーンに再開でき、任意のタイムスタンプの単一静止画は低コストです。すべて(フォント、テクスチャ)が読み込まれたら、window.TOTAL、window.END_T、window.__ready = trueを設定します。

film.html

黒いボディ、ゼロマージン、overflow: hidden。threeとthree/addons/をnode_modules/three/...にポイントするimport map。#subwrap / #sub字幕オーバーレイ。<script type="module" src="/scene.js">。

レンダラー

const W = 1080, H = 1920;
const renderer = new THREE.WebGLRenderer({ antialias: false, preserveDrawingBuffer: true, powerPreference: 'high-performance' });
renderer.setPixelRatio(1); renderer.setSize(W, H); renderer.toneMapping = THREE.NoToneMapping;
document.body.appendChild(renderer.domElement);

const depthTex = new THREE.DepthTexture(W, H); depthTex.type = THREE.UnsignedIntType;
const linear = { type: THREE.HalfFloatType, colorSpace: THREE.LinearSRGBColorSpace };
const rtScene = new THREE.WebGLRenderTarget(W, H, { ...linear, samples: 4, depthTexture: depthTex });
const rt = (w, h) => new THREE.WebGLRenderTarget(w, h, linear);
const rtDof = rt(W, H);
const rtB1 = rt(W >> 2, H >> 2), rtB1b = rt(W >> 2, H >> 2);   // ブラーム 1/4
const rtB2 = rt(W >> 3, H >> 3), rtB2b = rt(W >> 3, H >> 3);   // ブラーム 1/8

const quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2)); quad.frustumCulled = false;
const qScene = new THREE.Scene(); qScene.add(quad);
const qCam = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
const pass = (mat, target) => { quad.material = mat; renderer.setRenderTarget(target); renderer.render(qScene, qCam); };
const VERT = 'varying vec2 vUv; void main(){ vUv = uv; gl_Position = vec4(position.xy, 0.0, 1.0); }';

camera = new THREE.PerspectiveCamera(30, W / H, 1, 500);   // near/farはDOFシェーダーのuNear/uFarと等しくなければなりません

シーンは線形HDRターゲット(ハーフフロート、4倍MSAA、深度テクスチャ)にレンダリングされるため、スクリーンとライトは1.0を超えることができます。次に被写界深度、次にHDRでのブラーム、そしてトーンマッピングとガンマ補正を行う1つのコンポジットパス。このチェーンこそが「映画的」と「WebGLデモ」の違いです。

setT内部の作業順序

  1. キャラクターのポーズを取る
  2. scene.updateMatrixWorld(true)
  3. 接地とアタッチメント(プロップを手に、座席を床に)
  4. updateMatrixWorldを再度実行
  5. アンカーを更新(頭、手、プロップ、スクリーンのワールド位置)
  6. ショットリストからカメラを配置
  7. 字幕を更新
  8. シーンレンダリング → DOF → ダスト → ブライトパス → ブラー → キャンバスにコンポジット

ポストプロセッシング

被写界深度。 64点Vogelディスクを使用した深度認識ギャザー。明るいサンプルは追加の重みが与えられ、ハイライトがボケディスクに変わります。ぼやけた背景は、シャープな前景ににじみ出ることは許可されません。

uniform sampler2D tColor, tDepth;
uniform vec2 uRes;
uniform float uNear, uFar, uFocus, uAperture, uMaxCoc;
varying vec2 vUv;

float linDepth(float d) { float z = d * 2. - 1.; return 2. * uNear * uFar / (uFar + uNear - z * (uFar - uNear)); }
float coc(float z) { return min(uMaxCoc, uAperture * abs(1. / uFocus - 1. / z)); }

void main() {
  float z0 = linDepth(texture2D(tDepth, vUv).x);
  float c0 = coc(z0);
  float wsum = 1.0 / max(c0 * c0, 1.0);
  vec3 acc = texture2D(tColor, vUv).rgb * wsum;
  const int N = 64;
  for (int i = 0; i < N; i++) {
    float fi = float(i) + 0.5;
    float r = sqrt(fi / float(N));
    float a = fi * 2.39996323;
    vec2 off = vec2(cos(a), sin(a)) * r * uMaxCoc;
    float dist = length(off);
    vec2 uv = vUv + off / uRes;
    float zs = linDepth(texture2D(tDepth, uv).x);
    float cs = coc(zs);
    float eff = (zs > z0) ? min(cs, c0) : cs;            // 背景はシャープな前景を覆うことはできません
    float w = smoothstep(dist - 1.0, dist + 1.0, eff) / max(eff * eff, 1.0);
    vec3 s = texture2D(tColor, uv).rgb;
    w *= 1.0 + 1.6 * clamp(dot(s, vec3(0.3, 0.5, 0.2)) - 0.8, 0.0, 6.0);   // ボケブースト
    acc += s * w; wsum += w;
  }
  gl_FragColor = vec4(acc / wsum, 1.0);
}
  • uFocus: フォーカスアンカーからカメラまでの距離。視野方向に沿って測定され、フレームごとに再計算されます。
  • uAperture: 約1000~2500。値が大きいほど被写界深度が浅くなります。クローズアップでは高く、ワイドでは低く。デフォルトは中程度に設定。強いブラーはInstagramフィルターのように見えます。
  • uMaxCoc: 22 px。

ブラーム。

  • ブライトパス: `c * clamp((max(r,g,b

関連スキル