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 + ffprobe | PATH上か既知の場所に。 |
| three.js | npm 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内部の作業順序
- キャラクターのポーズを取る
scene.updateMatrixWorld(true)- 接地とアタッチメント(プロップを手に、座席を床に)
updateMatrixWorldを再度実行- アンカーを更新(頭、手、プロップ、スクリーンのワールド位置)
- ショットリストからカメラを配置
- 字幕を更新
- シーンレンダリング → 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