Site Reels: site → MP4 → deck
요구 사항
| 필요한 항목 | 내용 | 확인 방법 |
|---|---|---|
| 모든 기능 | GPU와 가시적인 데스크톱이 있는 Windows, macOS 또는 Linux (Chrome이 headful 모드로 실행됨) | n/a |
| 모든 기능 | Node.js 18+ | node -v |
| 모든 기능 | PATH에 ffmpeg + ffprobe (또는 FFMPEG=<전체 경로>) | ffmpeg -version |
| 모든 기능 | Chrome, Chromium 또는 Edge (자동 감지, 아래 참조) | node -e "import('./scripts/find-chrome.mjs').then(m=>console.log(m.findChrome()))" |
record.mjs | puppeteer-core + puppeteer-screen-recorder (npm, 프로젝트 내) | npm ls puppeteer-core puppeteer-screen-recorder |
snap.mjs | puppeteer-core + timesnap (timeweb 포함) | npm ls timesnap |
| 쇼릴 | Remotion 프로젝트 | npx remotion --version |
| 감사 (선택 사항) | ImageMagick | magick -version |
| Figma 업로드 | Figma MCP (use_figma, upload_assets) + 스킬 figma-use, figma-use-slides | MCP 도구 목록 |
Chrome 감지 (scripts/find-chrome.mjs, record.mjs 및 snap.mjs에서 사용):
CHROME 환경 변수가 설정된 경우 해당 경로 사용; 그렇지 않으면 설치된 Chrome/Edge (Windows: Program Files 및 LocalAppData; macOS: /Applications 및 ~/Applications; Linux: PATH의 google-chrome, chromium, microsoft-edge); 그 외에는 가장 최신의 ms-playwright/chromium-<N> (또는 PLAYWRIGHT_BROWSERS_PATH); 그래도 없으면 중단하고 CHROME 설정 방법 또는 npx playwright install chromium 실행 방법을 안내합니다. Windows에서는 --use-angle=d3d11을 추가하고, 다른 OS에서는 Chrome이 기본 GPU 백엔드를 유지합니다.
기능 설명
로컬에서 실행 중인 사이트의 스크립트된 장면을 녹화하여 Figma에 바로 사용할 수 있는 MP4 (1920×1080, 30fps, x264, < 100MB)를 생성합니다. 검증된 엔진: puppeteer-core + puppeteer-screen-recorder (CDP screencast + ffmpeg). 화면 녹화를 사용하지 않습니다.
실행
mkdir <proj>-reels && cd <proj>-reels && npm init -y && npm i puppeteer-core puppeteer-screen-recorder
mkdir -p scripts && cp <skill-dir>/scripts/{record,find-chrome}.mjs scripts/
cp <skill-dir>/scripts/scenes.example.mjs scenes.mjs # 장면 편집
# 사이트 저장소에서: npx next build && npx next start -p 3100 (프로덕션 빌드: WebGL이 더 부드러움)
OUT="<출력 폴더>/Videos" node scripts/record.mjs scenes.mjs [scene ...]
프로젝트로 복사된 스크립트를 실행합니다: ESM이 puppeteer-core를 cwd가 아닌 스크립트 폴더에서 확인합니다.
환경 변수: BASE (기본값 http://localhost:3100), OUT, CHROME, FFMPEG, W, H, DPR, MOBILE=1 (터치 에뮬레이션), KEEP_UI=1 (내비게이션 유지), MIN_FPS (24). 휴대폰 4:5: W=360 H=450 DPR=2 MOBILE=1은 720x900을 생성합니다.
각 장면 = { name, path, theme?, trim?, crf?, css?, prepare(page, ctx), act(page, ctx) }.
prepare는 녹화 전에 실행 (섹션으로 스크롤, 3D 로딩 대기); act는 비디오에 기록됩니다.
ctx에는 wait, scrollTo(page, y|selector, ms) 및 scrollThrough(page, selector|null, ms, from, to) (easing 포함)가 있습니다. 로드 시 실행되는 인트로: act 내에서 page.reload()를 사용하고 trim으로 로딩을 잘라냅니다.
작동하는 브랜드 스크립트: 로고/히어로 인트로 (라이트 및 다크), 히어로 미디어 교체, 스크롤 시 카메라 진입이 있는 3D 섹션, 몰입형 경로 (산책), 푸터 워드마크 및 전체 페이지 한 번 통과.
57개 장면, 각 1255초.
전달 전 감사 (종료 코드를 신뢰하지 마십시오)
- 비디오별 접촉 시트:
ffmpeg -i x.mp4 -vf "fps=6/<dur>,scale=480:-1,tile=3x2" -frames:v 1 sheet.png그리고 확인. - 19초에 1MB = 빈 장면. > 100MB = Figma에 들어가지 않음 (장면
crf로 수정; 입자와 트레일이 있는 장면은 24 사용). - 여러 시점에서 상단 스트립 (
crop=1920:120:0:0)을 확인하여 내비게이션 노출 감지. LOW-FPS에 시작/끝에 긴 멈춤이 있으면 오경보 (계측기가 정지 프레임을 계산함); 접촉 시트 확인.
알려진 문제점
- 숨겨진 창이 rAF를 정지 → 프레임 동결. 실행 시 이미
--disable-backgrounding-occluded-windows --disable-renderer-backgrounding --disable-background-timer-throttling --disable-features=CalculateNativeWinOcclusion를 전달합니다.headless: false를 의도적으로 사용: headless 모드는 SwiftShader로 폴백되어 WebGL이 끊깁니다. - Hide-UI CSS가 리로드 시 및 React 하이드레이션이
<head>를 지울 때 사라짐 → 스크립트가 100ms마다<style id="reels-hide">를 body에 다시 첨부합니다. - 잘못된 선택자 하나가 전체 규칙을 무효화함: JS 템플릿 리터럴의
\[는[가 되고.z-[60]는header, nav, .z-[60]를 무효화합니다. 위험한 선택자당 하나의 규칙. [data-cursor]를 숨기지 마십시오: 사이트가 3D 캔버스와 캐러셀을 이 속성으로 태그합니다. 사용자 정의 커서 요소를 대상으로 지정 (fixed + mix-blend + pointer-events-none).- 지연 로딩 3D (IntersectionObserver + 동적 가져오기): 섹션에 도달한 후 5~7초 대기.
preserveDrawingBuffer없이 WebGL 캔버스를 읽으면 빈 값이 반환됨: 고정 대기 또는 스크린샷 사용. - Screencast는 ~32개의 고유 프레임/초를 전달 → 30fps로 녹화 (60fps는 불규칙한 반복 프레임을 생성). 기본
trim0.3초는 첫 번째 캐시된 프레임을 제거합니다. - 로드된 머신에서 Screencast 끊김 (Photoshop 실행, 병렬 렌더): MP4가 "30fps"로 표시되지만 9~18개의 고유 프레임/초.
record.mjs가 측정 (mpdecimate=hi=64:lo=24:frac=0.05)하고MIN_FPS(24) 미만이면 최대 2회 재녹화. WebGL/3D 플레이트는snap.mjs를 선호합니다. - 쿠키 배너:
prepare에서 수락 ("Accept/Aceitar/OK"를 일반 클릭) 후 녹화. - 스크롤이 없는 도구 페이지 (생성기, 효과 편집기)는 정지 상태로 녹화됨: 장면이 인터랙션해야 함 (
input[type=file]에 파일 업로드, 버튼 텍스트로 프리셋 클릭). 사용자 정의 슬라이더는 합성input에 반응하지 않음.
60fps 고정 플레이트: snap.mjs (결정적)
scripts/snap.mjs는 timeweb (timesnap의 가상 클록: rAF, performance.now, setTimeout 및 video)을 주입하고, 프레임당 1/60초씩 진행하며 스크린샷을 찍습니다. 머신 부하와 무관합니다.
장면: { name, path, theme, css, dur, wait (3D 로딩을 위한 실제 ms), pre (스크롤 전 가상 ms, 2000; **로드 시 실행되는 인트로의 경우 0**), start (건너뛸 가상 s), scroll: { sel, from, to, at, len } | { bottom: [from, to] in screen heights }, frame(page, t) (예: 마우스) }. 스크롤은 가상 시간의 함수입니다.
npm i puppeteer-core timesnap && mkdir -p scripts
cp <skill-dir>/scripts/{snap,find-chrome,patch-timeweb}.mjs scripts/
npm pkg set scripts.postinstall="node scripts/patch-timeweb.mjs" && npm run postinstall
FPS=60 OUT=... node scripts/snap.mjs scenes-snap.mjs [scene]
timesnap()자체를 사용하지 마십시오: Next + R3F + Lenis와 함께 사용하면 빈 페이지를 기록하고 WebGL 히어로에 동결된 구간을 남깁니다 (rAF가 실행되지 않음). 자체 루프 (goTo + scrollTo + 스크린샷)가 모든 곳에서 작동했습니다: 60/60 고유 프레임.- timeweb 패치는 필수:
video.play()를 Promise 및playing이벤트가 없는 버전으로 교체 →video.play().catch()가 throw되고 페이지가 빈 상태가 됩니다. preserveDrawingBuffer: true가 getContext에 강제 적용 (preserve: false로 해제 가능).- 프레임은 OS 임시 디렉터리로 이동되며, 동기화된 드라이브로 절대 이동하지 않습니다 (수천 개의 jpg 동기화 방지).
- GPU 작업을 병렬로 실행하지 마십시오 (Remotion 렌더, Photoshop): WebGL 컨텍스트를 잃고 몇 초간 배경만 기록합니다. 스크립트는
FLASH(휘도로 프레임 깜박임)를 경고합니다. 접촉 시트에서만 긴 빈 구간이 나타납니다. 확인. - 실제 부드러움 감사: 전체 해상도에서 이웃 프레임 간
magick compare -metric MAE. 0 = 반복 프레임. - 전달:
scripts/deliver.example.sh(플레이트 복사 및 이름 변경;V=필요)는 reveal, 루프 (loop.sh) 및 깨끗한 3D를 빌드하고, 95MB 미만이 될 때까지 crf를 3씩 증가시킵니다.
Figma 슬라이드에 업로드 (숨김, 팀 방해 없음)
MP4는 MCP를 통해 들어가지 않습니다: use_figma는 네트워크가 없고 Plugin API는 바이트에서 비디오를 생성하지 않으며, upload_assets는 이미지(≤ 10MB)만 허용합니다. 따라서:
- 각 비디오의 커버:
ffmpeg -ss <t> -i x.mp4 -frames:v 1 -q:v 2 _capas/x.jpg. use_figma(스킬figma-use+figma-use-slides): 마지막 슬라이드 전에createSlideRow(idx),row.name = 'WIP · Motion (hidden)', 비디오당 하나의createSlide(idx, i)(isSkippedSlide = true, 검은 배경,VIDEO · x.mp4라는 이름의 1920×1080 사각형 및speakerNotes에 지침).- 사각형의
nodeIds로upload_assets→ 커버의 multipart POST. - 사용자가 MP4를 사각형으로 드래그 (커버를 비디오로 교체). 명확히 말하고, 삽입된 비디오를 약속하지 마십시오.
getSlideGrid()는 건너뛴 슬라이드를 생략합니다: SLIDE_GRID.children[i].children을 통해 구조를 확인하십시오.
브랜드 쇼릴 (Remotion): 플레이트 + 디자인된 장면
브랜딩 쇼릴 (로고, 색상, 타이포그래피, 레이아웃, 매니페스토, 목업)의 경우, 사이트에서 깨끗한 플레이트만 녹화하고 (텍스트 없음: 장면별 CSS로 제목/내비게이션 숨김) 나머지는 Remotion에서 구성합니다 (프레임별 렌더, 지터 없음). src/fx.tsx에 가질 가치가 있는 재사용 가능한 빌딩 블록: Mist, MistWords, Grain, KenBurns, Plate, nevoa() (안개) 전환, 브랜드 데이터는 brand.ts에.
- 시각적 아이덴티티는 덱에서 가져오고, 사이트에서 가져오지 마십시오: 슬라이드에서 공식 타이포그래피와 팔레트를 확인하십시오 (사이트는 브랜드 북에서 벗어날 수 있음).
- 작동하는 장면: 안개 속의 오프닝 문구, 녹화된 리빌, 로고 구성 (
evolvePath+ 로고 래스터에서 측정된 가이드), 문구별 매니페스토, 가변wght축이 있는 기둥, 밴드 형태의 팔레트, 타이포그래피, 계층형 그래픽,mix-blend-difference제목이 있는 3D, 스크롤 레이아웃 열, 1/2/3-up 목업 몽타주, 이름에 동기화된 제품, 워드마크로 마무리. - 스틸로 검토 (
npx remotion still ... --frame=N+ 몽타주) 후 렌더링. --concurrency=3으로 렌더링: 8에서 Photoshop이 열려 있으면 32GB 머신의 메모리가 부족해짐.- 루프:
scripts/loop.sh in out [start] [dur] [crossfade]. 첫 번째 프레임과 마지막 프레임 간magick compare -metric RMSE로 이음새 확인 (< 0.5% = 보이지 않음). - 나쁜 플레이트는 폐기: 회전하는 회전체는 움직임을 보여주지 않음; 세로 캔버스용으로 프레이밍된 카메라는 16:9에서 작게 보임.
- 격렬한 릴 (승인된 리듬): 25
35초, 60fps, 120 BPM 그리드 (30프레임)에서 하드 컷이 있는6프레임마다 이미지 버스트, 그리드 위의 거대한 로고 크롭, 깜박이는 글꼴 두께, 재배열되는 팔레트 블록, 원형 마스크의 초상화, 그래픽 버스트, 운동성 문구, 한 박자 목업, ~2.5초 동안 유지되는 클로징 로고. 레이블 제로: 텍스트는 그래픽으로만 사용. 느린 1:40 쇼릴은 거부됨.Series, 3 - 소프트/스모키 릴: 히어로 연기를 로고나 사진 없이 녹화, 순수 흰색 위 검은색 (장면에
storage= localStorage의 params 및block: ['assets/logo.png']; 빈 PNG는 투명해야 함; 비디오를 차단하지 마십시오: 깨진 비디오는 timeweb을 중단시킴). 플레이트를mix-blend-mode: multiply오버레이로 사용: 장면 시작 시 열리고, 반전 버전(ffmpeg -vf reverse)으로 닫힘; 컷은 검은색에 떨어짐. ~1.1초 (비율 2)에서 전환, 또는 장면의 절반이 검은색으로 진행. - Figma 슬라이드 레이아웃 조립: 슬라이드 (건너뛴)를 복제하고, 각 카드를 배경/로고/텍스트만 표시되도록 3배 복제하고, 각 노드를
download_assets(스케일 2)로 다운로드하고 복제본을 삭제. 슬라이드는 투명도를 내보내지 않음: 슬라이드 검은색과 흰색으로 로고/텍스트를 내보내고 차이로 알파를 추출 (ImageMagick).use_figma를 통해 base64를 반환하지 마십시오 (컨텍스트가 깨짐).
제품 쇼릴 (실제 앱 촬영)
화면을 실제로 촬영 (Playwright + CDP screencast at 2x, 단계 goto/move/click/type/scroll/wait, 포인터 궤적을 <id>.cursor.json으로 저장)하고, 커서를 Remotion에서 위에 다시 그림.
거부됨, 반복하지 마십시오:
- 디자인된 릴을 실제 화면에 접착 (일러스트레이션 오프닝 + 촬영된 중간 + 그려진 CTA): 두 가지 언어, 캐릭터가 사라졌다가 돌아오고, 액트 사이의 검은 카드가 리듬을 중단. 비디오당 하나의 언어.
- 16:9 창의 작고 속도가 빨라진 제품 화면: 피드와 순위가 읽을 수 없는 벽이 됨.
- 화면을 자르는 줌 및 비디오 위에 텍스트 스탬프 (거대한 동사, 01/07 카운터, 그라데이션): 침입적.
- 화면에서 빈 대기 ("waiting…"): 바로 액션으로 컷.
- 모호한 카피: 명백해야 하고 청중의 평범한 구어체로 작성되어야 함.
작동하는 형식: 액션당, 먼저 텍스트만, 중앙 정렬 및 짧게 (2비트), 그런 다음 전체 화면, 크롭되지 않음, 캡처 비율의 중앙 창, 깨끗한 배경. 신중한 텍스트 (굵기 700, 약간의 블러로 단어별 진입), 카운터 없음, 그라데이션 없음. 트랙 그리드에서 컷 (14프레임 = 128.57 BPM). 최대 40초.
효과가 있었던 기술:
- 캡처 속도 높이기:
<OffthreadVideo trimBefore={from} playbackRate={rate}>를 사용하고 클릭 프레임을/ rate로 다시 계산 (사운드 및 커서 링). - 중간에서 다른 컴포지션 시작:
<Sequence from={X}><Sequence from={-start}><Comp/></Sequence></Sequence>. 해당 구간의 첫 번째 프레임 확인: 공유 요소가 "날아 들어오면" 느슨해짐. 몇 프레임 건너뛰고 그 앞에 떨어진 충격음을 다시 배치. - 모듈 수준 타임라인이 있는 컴포지션별 언어: Root 전에 가져온 자체 항목 파일에 의해 설정된
globalThis플래그. - 컷을 가로지르는 연속 트랙: 배드에서
trimBefore를 사용하여 이전 구간이 중단된 지점부터 계속. - Loudness: 렌더링, 측정 및 2-pass
loudnorm(-14 LUFS / -1 dBTP)으로 정규화, 비디오 복사. 마지막에만 측정: Remotion의 원시 믹스는 0 dBFS를 초과함. - 감사: 모든 렌더의 타임스탬프 접촉 시트 (
fps=2,drawtext=%{pts\:hms},tile=8x6) 및 모든 구간 접합부의 조밀한 시트 (fps=6). - git 없고 다른 세션이 동시에 편집하는 프로젝트: 새 파일에 새 컷, 공유 파일 편집 전
*.bak-*백업, 사용자에게 알림.
포트폴리오 카드 및 스튜디오 릴
record.mjswithKEEP_UI=1(카드가 사이트를 그대로 표시) 및 위의 휴대폰 프리셋.- 인벤토리 파일에서 생성된 장면 (라이브 URL이 있는 공개 항목만); 라이브 URL이 없음 = 스크린샷의 Ken Burns.
- 카드 패키지: 끝에서 시작으로의 크로스페이드 루프, KB 제한에 대한 2-pass 비트레이트, 함께 제공되는 VP9 WebM, 프레임 1의 WebP 포스터.
- 웹용 알파가 있는 로고: `-
…
[번역이 잘렸습니다]