Communitygithub.com

devlucasalm/motion-promo-video-skill

建立高階動態設計宣傳影片(9:16 / 16:9,60fps,含音效與音樂),適用於產品、SaaS、手錶等,使用 HTML + Playwright + ffmpeg。適合製作動態影片、廣告影片、Reels、動畫宣傳。

motion-promo-video-skill 是什麼?

此技能可讓 AI 代理從 HTML 範本產生專業級行銷宣傳影片(MP4 格式),透過 Playwright 與 ffmpeg 進行無頭渲染。它支援 9:16(Instagram Reels)與 16:9 格式,實現 60fps 流暢動畫,內建運動模糊,並提供合成音效(SFX)與背景音樂。技能核心是一個確定性 HTML 引擎,其中所有動畫(位置、縮放、透明度、文字顯示)均由單一 `seek(T)` 函數驅動,該函數以時間為參數,因此渲染完全可重複且可中斷回復。使用者只需提供簡短的規格摘要(格式、受眾、品牌色調、產品照片),AI 代理會自動處理摳圖、背景、字體、動態分鏡與音效設計。輸出包含一個 MP4 檔案(H.264 編碼),適用於 Claude Code、Cursor 或 Codex 等平台,非常適合行銷團隊、SaaS 新創公司或任何需要快速製作品牌宣傳影片的場景。

相容平台~Claude Code~Codex CLI~Cursor
npx skills add devlucasalm/motion-promo-video-skill

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

Motion promo video (HTML → MP4)

產出:可直接使用的 MP4(H.264、yuv420p、AAC),透過 SendUserFile 傳送。請用繁體中文(zh-TW)簡短回覆。

1. 最低限度的簡報(不要問太多問題)

預設設定如下,僅詢問會改變結果的項目:

  • 格式:預設 9:16(1080×1920);若要求則為 16:9(1920×1080)。長度:Instagram 廣告 20–35 秒;節奏 慢速(每個步驟約 1.5–4 秒)——使用者曾抱怨「看起來像 2 倍速」。
  • 受眾:年輕 / SaaS → 明亮的 SFX(鈴聲、爆裂聲、點擊聲)。年長 / 高階(手錶、珠寶)→ 低調:襯墊音、柔和衝擊、呼呼聲、閃爍聲;絕對不要 兒童爆裂聲。
  • 品牌:色板、字體、Logo、CTA(WhatsApp / 網址)、螢幕截圖 / 照片。音樂:若使用者提及某首歌曲(Spotify 等),你不得取得該音訊——請告知使用者,使用原創配樂,並提供若使用者上傳 mp3 則可進行同步(並提醒廣告授權)。

2. 準備素材

  • 產品照片:使用 rembg(isnet-general-use 模型)去背 → 產生含 Alpha 色版的 PNG,以呈現「3D」效果(漂浮、帶有 perspective 的 rotateY / rotateX、透過 PNG Alpha 色版遮罩的 -webkit-mask-image 光暈)。
  • 低解析度螢幕截圖:使用 Lanczos 3× 放大 + UnsharpMask。當使用者要求「不含廣告」時,移除原始照片中的廣告文字。
  • 字體:Inter / Inter Display ExtraBold(SaaS,扁平風格)、Lora serif + 金色斜體(高階)、Poppins。

3. 使用者已核准的風格

  • 色塊、大型卡片、帶遮罩文字逐字揭示的巨型字體、大量連續運動(慢速視差、慢速縮放 1→1.15、星星 / 粒子)、透過與節拍同步的 3 色面板擦拭進行切換。
  • 無 相機晃動(使用者拒絕了抖動):晃動最多 ×0.12,無相機旋轉,偏移 ≤3 px。
  • 敘事結構:鉤子 / 問題 → 解決方案 → 2–3 個功能 / 整合 → 證明 / 好處 → CTA。每個場景 3–5 秒,之間以擦拭切換。在 SaaS 中,具體 呈現問題(未回覆的訊息、損失的銷售金額 −$)再提出解決方案。
  • 文字簡短,每行 2–4 個詞,使用繁體中文,其中一個詞以顏色 / 斜體強調。遵守 Instagram 的安全區域(在 9:16 中,頂部約避開 250 px,底部約避開 400 px 以放置 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=根據 cam(t,T) 的 translate/scale;s.upd(t,obj,T) */}
// wipes: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);/* 最終淡入黑色 */}

固定圖層位於世界之上:灰塵/粒子畫布、擦拭、底片顆粒(alpha .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 手錶中已核准):SVG 760 px,包含 60 個刻度(每 5 個較粗/金色)、金色弧線繪製(stroke-dashoffset)、指針每 0.5 秒以 E.outBack 前進 6°、已走過的刻度點亮;文字「時間對所有人都流逝。/ 少數人留下印記。」;旋轉的 ✦ 小星星。當產品允許時,重複使用此類主題開場。 可變速度時間軸:SEG=[[start,end,factor],…] 應用於 seek 以及音訊的 M(t) 函數,使 SFX 跟隨剪輯點。

5. 渲染(確定性、可中斷回復)

  • Playwright Chromium viewport 1080×1920(或 1920×1080),page.evaluate('seek(t)') + screenshot(type='jpeg',quality=92)。
  • 60 fps 搭配運動模糊:每個影格 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 個影格為一個 chunk,存放在 seg/ 目錄中,使用 2 個並行處理(偶數/奇數 chunks),跳過已存在的 chunk(容器可能重啟)。使用 setsid nohup ./go.sh & 執行,並以 Monitor 在 until 迴圈中檢查完成標記檔案(前景 sleep 會被阻斷;若超過 30 分鐘則重新啟動 Monitor)。約 0.38 秒/張截圖,2 個 CPU:24 秒 ≈ 1440 個影格 ≈ 25–30 分鐘。
  • 只改了開頭?只刪除受影響的 chunks 並重新渲染;如果只變更音訊,則使用 -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。
  • 在完整渲染前,先對關鍵時間點擷取 2–4 張 seek() 的截圖,並檢查兩種格式的文字裁切/溢位(之前曾發生字體過大問題),以及 JS 錯誤(page.on('pageerror'))。

6. 音訊(numpy/scipy,合成)

輔助函數:lp/hp/bp(butter)、add(buf,t,x,g)、tt(d);音效:thump(衰減的正弦波 46–96 Hz)、impact(thump + 低通 700 雜訊)、riser(頻帶上升的雜訊)、swish(帶通 1.8–5.5 kHz 雜訊,sin² 窗口)、shimmer(具衰減的非和聲泛音)、tick(約 900 Hz 正弦波,decay 90)、pad(3 個失諧正弦波,低通 1.4 kHz,帶淡入)、pluck;透過與衰減雜訊進行卷積實現殘響。

  • SaaS / 年輕:明亮的鈴聲、爆裂聲、點擊聲、呼呼聲——不要使用沉重的低音(使用者厭惡「全是低音」)。
  • 高階/年長:在擦拭時使用 riser+impact、在文字揭示時使用 swish、在產品出現時使用 shimmer、在列表時使用 ticks、在手錶開場時每 0.5 秒一個滴答聲、用以緩慢和弦(Dm–Bb–F–C)構成的 pads、約 90 bpm 的柔和節拍。80–90 bpm 的節拍與每 4 秒的剪輯點搭配良好。
  • 母帶處理(避免「爆音」):絕不對熱混音使用 tanh。應進行 x=x/np.abs(x).max()*.85,儲存 wav 後執行 ffmpeg -af loudnorm=I=-16:TP=-2:LRA=11 -ar 44100。確認峰值 < -1 dBFS 且 crest factor > 10 dB。

7. 交付與迭代

傳送 MP4 並附上一行說明;提及變更內容以及未檢查的事項(例如:「已檢查影格與指標,未聆聽音訊」)。常見回饋與回應:「太快」→ 將時間軸縮放 ×1.5;「抖動」→ 減少晃動;「聲音不好」→ 切換 SFX 設定檔(明亮 vs. 低調);「爆音」→ 僅重新處理音訊並重新封裝;「我喜歡 v1 的 X 動畫」→ 恢復 X 場景,僅重新渲染受影響的 chunks。 若使用者要求可重複使用的提示/文件,請生成一份包含此流程的文件。

相關技能