モーションプロモーションビデオ (HTML → MP4)
納品物:MP4(H.264、yuv420p、AAC)完成品をSendUserFileで送信。応答は短く、日本語(ja)で行う。
1. 最小限のブリーフィング(質問しすぎない)
デフォルトを設定し、結果に影響するものだけを尋ねる:
- フォーマット:9:16(1080×1920)がデフォルト。16:9(1920×1080)は要望があれば。長さ:インスタ広告20~35秒。ペーシング:遅め(各ステップ約1.5~4秒)— ユーザーから「2倍速に見える」とクレームがあった。
- 対象視聴者:若年層/SaaS → 明るいSFX(ベル、ポップ、クリック)。年齢層が高い/プレミアム(時計、ジュエリー)→ 落ち着いた:パッド、ソフトインパクト、ウィッシュ、シャマー。子供向けポップは不可。
- ブランド:パレット、フォント、ロゴ、CTA(WhatsApp/URL)、スクリーンショット/写真。音楽:ユーザーが曲(Spotifyなど)を指定した場合、オーディオにはアクセスせず、その旨を伝え、オリジナル曲を使用する。ユーザーがmp3を送信した場合は同期を提案する(広告ライセンスにも注意)。
2. アセットの準備
- 商品写真:
rembg(isnet-general-useモデル)で背景除去 → アルファ付きPNG(「3D」効果用:浮遊、rotateY/rotateX+perspective、-webkit-mask-imageでPNGのアルファをマスクした輝きsheen)。 - 低解像度のスクリーンショット:Lanczos 3倍アップスケール+UnsharpMask。ユーザーが「広告なし」を希望した場合は、元の写真から広告テキストを削除する。
- フォント:Inter / Inter Display ExtraBold(SaaS、フラットスタイル)、Lora serif+ゴールドイタリック(プレミアム)、Poppins。
3. ユーザー承認済みのスタイル
- 単色ブロック、大きなカード、マスクされた単語ごとのリビールによる巨大タイポグラフィ、継続的な動き(ゆっくりとしたパララックス、1→1.15へのゆっくりズーム、星/パーティクル)、ビートに同期したカラフルな3パネルワイプによるカット。
- カメラのブレはなし(ユーザーから却下済み):ブレは最大×0.12、カメラの回転なし、ドリフト3px以下。
- ストーリー構成:フック/問題 → 解決策 → 2~3の機能/統合 → 証明/メリット → CTA。各シーン3~5秒、シーン間はワイプ。SaaSの場合は、解決策の前に具体的に問題を表示する(未読メッセージ、販売損失−R$など)。
- テキストは短く、1行あたり2~4単語、日本語(ja)。1単語を強調(色/イタリック)。インスタグラムのセーフゾーンを尊重する(9:16の場合、上部約250px、下部約400pxをCTA/キャプションのために避ける)。
4. エンジン(決定論的HTML)
video.htmlファイルにwindow.seek(T)を実装し、すべてを時間Tの関数として配置する(requestAnimationFrame/CSSアニメーションは使用しない)。コア:
const cl=(x,a=0,b=1)=>Math.min(b,Math.max(a,x)),lerp=(a,b,p)=>a+(b-a)*p;
const E={outBack:(p,c=1.8)=>1+(c+1)*Math.pow(p-1,3)+c*Math.pow(p-1,2),outExpo:p=>p>=1?1:1-Math.pow(2,-10*p),outQuart:p=>1-Math.pow(1-p,4),inOutQuart:p=>p<.5?8*p**4:1-Math.pow(-2*p+2,4)/2,inOutCubic:p=>p<.5?4*p**3:1-Math.pow(-2*p+2,3)/2,lin:p=>p,inCubic:p=>p**3,inOutSine:p=>-(Math.cos(Math.PI*p)-1)/2};
const ez=(T,a,d,f=E.outExpo)=>f(cl((T-a)/d));
function mk(parent,html,cls='',style=''){const d=document.createElement('div');d.className='abs '+cls;d.style.cssText=style;d.innerHTML=html;parent.appendChild(d);return d} // .abs{position:absolute;left:50%;top:50%}
function P(el,o){const x=o.x||0,y=o.y||0,s=o.s==null?1:o.s,op=o.o==null?1:o.o,r=o.r||0,rx=o.rx||0,ry=o.ry||0;el.style.transform=`perspective(2400px) translate(-50%,-50%) translate(${x}px,${y}px) rotateX(${rx}deg) rotateY(${ry}deg) rotate(${r}deg) scale(${s})`;el.style.opacity=op;el.style.visibility=op<=.003?'hidden':'visible'}
function words(el,arr){el.innerHTML=arr.map(w=>`<span class="mk"><span class="wd ${w[1]||''}">${w[0]}</span></span>`).join(' ')} // .mk{display:inline-block;overflow:hidden;padding:.14em .05em .24em;margin:-.14em -.05em -.24em}
function lineIO(el,T,tin,tout,step=.1,d=1.0){[...el.querySelectorAll('.wd')].forEach((w,i)=>{const a=ez(T,tin+i*step,d,E.outQuart),b=tout?ez(T,tout+i*step*.5,.5,E.inCubic):0;w.style.transform=`translateY(${(1-a)*118-b*118}%)`;w.style.opacity=cl(a*2.2,0,1)*(1-b)})}
function addScene(a,b,bg,build,upd,cam){/* シーン div.sc を作成、bg、inner; scenes.push({a,b,root,inner,obj:build(inner),upd,cam}) */}
function runScenes(T){/* Tが[a-.02,b+.02]の範囲内の場合のみ表示; inner.style.transform=translate/scale from cam(t,T); s.upd(t,obj,T) */}
// ワイプ:3パネル [暗色、ゴールド/アクセント、次のシーンの背景色]、各パネルは ez(T,t0+i*.09,.38,E.inOutQuart) で出現・消滅(t0=wipe-.55); 方向は x / xr を交互に
function seek(T){runScenes(T);drawWipes(T);drawFX(T);drawGrain(T);/* 最後にフェードアウト */}
ワールドの上の固定レイヤー:ダスト/パーティクル用キャンバス、ワイプ、フィルムグレイン(アルファ0.07)、ビネット、フェードイン/アウト用黒ベール。
プロダクトの3Dエフェクト:P(hero,{ry:Math.sin(T*.7)*8, rx:Math.sin(T*.5)*3, y:Math.sin(T*1.1)*14, s}) + sweep()(PNGでマスクされたmix-blend-mode:soft-lightの輝きの帯)+ 背面の放射状ゴールドグロー + drop-shadow シャドウ。
時計風オープニング(時計v1でユーザー承認済み):760pxのSVG、60個の目盛り(5個ごとに太く/ゴールド)、描画されるゴールドアーク(stroke-dashoffset)、0.5秒ごとに6°進む針(E.outBack)、通過済みの目盛りが点灯。テキスト「時間は誰にでも平等に流れる。 / しかし、刻むのはごく一部。」。回転する小さな星✦。プロダクトが許せば、このタイプのテーマオープニングを再利用する。
可変速度タイムライン:SEG=[[ini,fim,fator],…] を seek とオーディオの M(t) 関数の両方に適用し、SFXがカットに追従するようにする。
5. レンダリング(決定論的、再開可能)
- Playwright Chromium ビューポート 1080×1920(または1920×1080)、
page.evaluate('seek(t)')+screenshot(type='jpeg',quality=92)。 - 60fps モーションブラー:1フレームあたり2サブフレーム(t=(f+(s+.5)/2-.5)/60)、numpy float32で平均化 → JPEG q95 →
ffmpeg -f image2pipe -c:v mjpeg -i - -c:v libx264 -preset fast -crf 18 -pix_fmt yuv420p seg/NNNNN.mp4。 - 120フレーム単位のチャンクを
seg/に保存、2つの並列プロセス(偶数/奇数チャンク)、既存のチャンクはスキップ(コンテナが再起動しても可)。setsid nohup ./go.sh &で実行し、Monitorを使用してuntilループで完了ファイルを監視する(フォアグラウンドのsleepはブロックされるため、30分でタイムアウトした場合はMonitorを再アクティブ化する)。〜0.38秒/スクリーンショット、2CPU:24秒 ≈ 1440フレーム ≈ 25〜30分。 - 先頭部分だけ変更した場合?影響を受けるチャンクのみを削除して再レンダリング。オーディオだけ変更した場合は、
-c:v copyでリマックス。 - 連結:
ffmpeg -f concat -safe 0 -i list.txt -i audio.wav -c:v libx264 -crf 21 -pix_fmt yuv420p -c:a aac -b:a 192k -shortest -movflags +faststart out.mp4。 - 全体をレンダリングする前に、
seek()の2〜4枚のスクリーンショットを主要な時間で撮り、両方のフォーマットでのテキストのクリッピング/オーバーフロー(大きなフォントで発生したことがある)とJSエラー(page.on('pageerror'))を確認する。
6. オーディオ(numpy/scipy、シンセサイズ)
ヘルパー:lp/hp/bp(butter)、add(buf,t,x,g)、tt(d)。サウンド:thump(46〜96Hzの減衰サイン波)、impact(thump+LP 700Hzノイズ)、riser(周波数上昇するバンドノイズ)、swish(BP 1.8〜5.5kHzノイズ、sin²ウィンドウ)、shimmer(非調和部分音、減衰付き)、tick(約900Hzのサイン波、減衰90)、pad(3つのデチューンサイン波、LP 1.4kHz、フェード付き)、pluck。リバーブは減衰ノイズとの畳み込み。
- SaaS/若年層向け:明るいベル、ポップ、クリック、ウィッシュ — 重低音は使わない(ユーザーが「すべて低音」を嫌ったため)。
- プレミアム/年齢層高め向け:ワイプにはライザー+インパクト、テキストリビールにはスウィッシュ、プロダクト登場にはシャマー、リストにはティック、時計オープニングでは0.5秒ごとの時計のチック、ゆっくりとしたコード進行(Dm–Bb–F–C)のパッド、約90bpmのソフトビート。80〜90bpmのビートは4秒ごとのカットに適している。
- マスタリング(「音割れ」防止):ホットなミックスに
tanhを決して使わない。x=x/np.abs(x).max()*.85で処理し、wavを保存してffmpeg -af loudnorm=I=-16:TP=-2:LRA=11 -ar 44100を実行。ピークが −1 dBFS未満、クレストファクターが 10 dB超であることを確認する。
7. 納品とイテレーション
1行のキャプション付きでMP4を送信。変更点と未確認事項(例:「フレームとメトリクスは確認しましたが、音声は確認していません」)に言及する。一般的なフィードバックとその対応:
- 「速すぎる」→ タイムラインを1.5倍にスケーリング
- 「ブレている」→ ブレを低減
- 「音が悪い」→ SFXプロファイルを変更(明るい vs 落ち着いた)
- 「音割れしている」→ オーディオのみリマスタリングしてリマックス
- 「v1のアニメーションXが良かった」→ シーンXを復元し、影響を受けるチャンクのみ再レンダリング
ユーザーが再利用可能なプロンプト/ドキュメントを要求した場合は、このフローをまとめたドキュメントを生成する。