Communitygithub.com

HusseinRagab/motion-reels

9:16 모션 그래픽 릴(1080x1920)을 코드 드로잉 애니메이션으로 제작합니다. 헤드라인, 평면도 드로잉, 비포/애프터 와이프, 사진 푸시인, 소재 견본 등을 Playwright와 ffmpeg으로 프레임 단위로 렌더링합니다. 릴, 쇼츠 또는 세로형 모션 비디오를 만들거나, 스토리보드를 작성하거나, 렌더링하거나, 수정하라는 요청을 받았을 때 사용하세요.

motion-reels란 무엇인가요?

이 스킬은 인테리어 디자인 페이지를 위한 9:16 세로형 모션 그래픽 릴(Reels)을 처음부터 끝까지 제작하는 도구입니다. 각 릴은 독립적인 JavaScript 블록들로 구성되며, 각 블록은 헤드라인, 도면, 사진, 견본 등의 요소가 타임라인에 맞춰 등장하고 움직이는 방식을 정의합니다. Playwright를 사용해 각 프레임을 신선한 브라우저 페이지에서 렌더링하고, ffmpeg로 최종 영상을 조합합니다. 클라이언트나 소유자가 제공한 사진, 텍스트, 음성(선택 사항)을 기반으로 제작되며, 엄격한 모션 규칙(단어는 움직이지 않음, 카메라는 정적, 부드러운 슬라이드 등)을 따라 프리미엄하고 안정적인 느낌을 보장합니다. 최종 결과물은 LUFS 기준에 맞춰 노멀라이즈되며, 모든 단계에서 소유자의 승인을 받도록 게이트 시스템이 설계되어 있습니다. 주요 플랫폼은 Claude Code이지만, Python과 ffmpeg을 지원하는 모든 환경에서 사용 가능합니다.

지원 대상✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/HusseinRagab/reel-motion-kit/tree/HEAD/motion-reels

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

motion-reels은(는) 무엇을 하나요?

코드로 그려지는 세로형 릴 모션 그래픽. 각 릴은 하나 이상의 블록으로 구성됩니다. 블록은 헤드라인, 그림, 사진 및 견본을 배치하고 각 요소가 언제 나타날지를 지정하는 작은 JavaScript 파일입니다. 모든 타이밍은 단어 타임라인에서 비롯됩니다. 비트 시트(음성 없음) 또는 음성 녹음의 단어 타이밍입니다. 엔진은 모든 프레임을 신선한 브라우저 페이지에서 렌더링하고, 레이아웃을 확인한 후 음성 및/또는 음악과 함께 결과를 조합합니다.

페이지를 소유한 사람이 외관, 단어 및 게시물 내용을 결정합니다. Claude는 절대 아무것도 게시하지 않습니다.

설정 (한 번만)

  • Python 3.10+ 및 PATH에 ffmpeg/ffprobe가 필요합니다. 그런 다음 리포지토리의 setup.ps1 (Windows) 또는 setup.sh (macOS/Linux), 또는 pip install -r requirements.txt + python -m playwright install chromium (또는 환경 변수 MG_CHROME = 기존 Chrome 경로).
  • 음성만: pip install -r requirements-voice.txt (faster-whisper; 첫 실행 시 음성 모델 다운로드).
  • 테스트: python engine/selftest.py 실행 시 SELFTEST OK가 출력되어야 합니다 (예제 통과, 음성 테스트는 예상대로 실패, 아랍어 자막이 올바르게 분할되며, 짧은 렌더링 + 결함 검사 + 조합이 -14 LUFS에 도달하는지 확인). --quick는 렌더링을 건너뜁니다.

엔진 파일 (모두 engine/ 디렉토리): render.py (확인 / 스토리보드 / 스틸 / 비디오), beats.py, words.py, frames.py, faultscan.py, assemble.py, storyboard_sheet.py, make_grain.py, templates/ (셸, reel.js, reel.css, 글꼴).

릴당 하나의 폴더

<reel name>/
  brief.md          릴의 목적, 하나의 아이디어, 클릭 유도 문구, 사용 가능한 사진
  beats.txt         화면에 표시될 단어들, 한 줄에 하나의 비트 (또는 음성이 있는 경우 script.txt + take.wav)
  words.json        타임라인 (beats.py 또는 words.py에서 생성)
  photos/           소유자의 사진 (권한이 있는 것만)
  A.js, B.js ...    블록
  build/            확인 JSON, 스토리보드 스틸, 블록 비디오, render.json 파일, 연락처 시트
  <Reel>_REV01_<date>.mp4   최종 결과물; 이후 수정본은 _REV02로 제공되며, 덮어쓰지 않음

게이트 (절대 건너뛰거나 순서를 변경하지 않음; 각 ⛔에서 소유자의 승인을 위해 중단)

  1. 브리프. 릴당 하나의 아이디어 (방 공개, 소재 이야기, 평면도 변경, 팁). 사진과 핸들을 요청하세요. 각 사진이 보여주는 내용을 나열하세요. 처음 2초 안에 시청자를 사로잡으세요.
  2. 비트 시트 (beats.txt): 화면에 표시될 정확한 단어들을 순서대로, 한 줄에 하나의 비트로 작성합니다. 비트는 짧게 유지하세요 (2–7단어). 목록의 각 항목은 자체 줄에 배치하여 각각이 충분히 머물 시간을 갖도록 하세요. ⛔ 소유자가 단어를 승인합니다. 승인 후 단어 변경은 블록 타이밍을 다시 조정해야 함을 의미합니다 (음성이 있는 경우 음성도 다시 녹음). 변경 전에 이를 반드시告知하세요.
  3. 타임라인.
    • 음성 없음: python engine/beats.py beats.txt words.json (기본 속도 2.6단어/초 + 각 비트 후 0.7초; 줄 끝에 [+1.5]를 추가하면 더 오래 유지되고, [pause 2]는 새로운 내용 없이 유지됨).
    • 음성 (모든 TTS 또는 녹음, 블록당 한 테이크, 절대 편집하지 않음): python engine/words.py take.wav words.json 실행 후, 대본과 자막을 단어 단위로 비교합니다. 그림은 테이크에 맞춰지며, 그 반대는 절대 안 됩니다.
  4. ⛔ 스토리보드 스틸. 블록 레이아웃을 구축하고 (위치만으로 충분), check를 실행한 후 render.py A.js words.json storyboard build/sb 및 storyboard_sheet.py build/storyboard.png build/sb를 실행합니다. 시트를 자체 검토하고 (아래 목록 참조) 보냅니다. 소유자가 레이아웃을 승인하기 전까지는 아무것도 애니메이션되지 않습니다.
  5. 모션 구축. 모든 요소를 해당 요소를 명명하는 단어에 맞춰 큐잉합니다 (아래 규칙 참조). 모든 변경 후 check를 실행합니다.
  6. 렌더링. 블록당 render.py A.js words.json video build/A.mp4 --workers 4 (보내는 어떤 것에도 --draft 사용 금지). 모든 블록의 build/A.mp4.render.json은 "capture": "fresh"를 나타내고 다른 블록과 동일한 브라우저를 사용해야 합니다. 브라우저가 변경된 경우 모든 블록을 다시 렌더링하세요.
  7. 결함 검사. 모든 블록에 대해 faultscan.py build/A.mp4 --sheet build/A_contact.png를 실행하고, 최종 릴에 대해서도 실행합니다. 플래그가 지정된 모든 시간을 확인하세요. 검은 띠나 1프레임 플래시는 결함입니다. 중단하고 수정하세요. 어두운 사진 콘텐츠는 괜찮습니다.
  8. 조립. assemble.py blocks.json <Reel>_REV01_<date>.mp4 [--music bed.mp3]를 실행합니다. 측정된 음량을 출력합니다. 대상값(-14 LUFS 기본값)에 도달해야 하며, 실제 피크는 -1.5 dBTP 이하여야 합니다.
  9. ⛔ 검토. 릴과 그 연락처 시트를 보냅니다. 사진만 수정하는 경우 변경된 블록만 다시 렌더링하고 다음 _REV로 제공합니다. 소유자가 게시합니다.

모션 규칙 (이유: 차분하고 프리미엄한 느낌; 아무것도 떨리거나 읽기 어렵지 않아야 함)

  • 단어는 절대 움직이지 않습니다. 단어는 페이드 인(0.28초) 된 후 정지 상태를 유지합니다. 텍스트에 슬라이드, 스케일, 바운스 또는 드리프트 효과를 사용하지 마세요.
  • 헤드라인: hl(el, text, cue(...))는 각 단어가 타임라인의 해당 단어에 맞춰 페이드 인되도록 합니다. 헤드라인이 음성을 바꿔 말할 때는 {stream:true}를 사용하세요. 단어가 큐에서부터 왼쪽에서 오른쪽으로 0.1초 간격으로 나타납니다.
  • 카메라: 샷이 유지되는 동안은 완전히 정지; 샷 사이에서만 슬라이드합니다 (0.8초, ease-in-out). 9:16에서는 샷을 세로로 쌓아 배치하고 (샷 2는 y 1920, 샷 3은 y 3840) 아래로 슬라이드합니다: SHOTS=[[t,540,960,1],[t2,540,2880,1],...].
  • 사진은 천천히 푸시 인할 수 있습니다: push:[1,1.04] = 일정 속도, 최대 +6%를 초과하지 않음 (MOTION.PUSH_MAX; 엔진이 초과를 거부). 텍스트가 있는 사진에는 푸시를 사용하지 마세요.
  • 도면은 이름이 지정될 때 그려집니다: 참조선이 먼저 그려집니다. 벽이 창문과 문보다 먼저, 바닥선이 그로부터 측정된 높이보다 먼저. at('#walls', cue('Walls'), null, 'draw', 0.9); 요소당 0.6–1.0초.
  • 치수는 하나의 축척으로. 평면도에 두 개의 치수가 표시되면 둘 다 동일한 축척을 사용합니다.
  • 단어가 명명하는 것을 레이블링하세요: 모든 숫자는 해당 단위를 명명합니다 ("3.2 m", 절대 "3.2"만 쓰지 않음).
  • 어디에서도 바운스, 오버슈트, 탄성 또는 스프링 이징을 사용하지 마세요. 모든 타이밍은 reel.js의 MOTION에서 가져옵니다. 블록 내에서 리터럴로 사용하지 말고 MOTION에서 변경하세요.
  • at()에 제공된 점선 SVG 요소는 큐 전에 표시됩니다. <g>로 감싸세요.

타이밍 토큰 (reel.js의 MOTION)

토큰값용도
WORD_FADE / WORD_LEAD0.28초 / 0.08초단어 페이드 인; 단어 바로 직전에 시작
STREAM0.10초스트림 모드 헤드라인 단어 간격
REVEAL / EXIT0.5초 / 0.4초블록, 칩, 견본, 사진 등장/퇴장
draw (at(...,'draw',dur))0.6–1.0초그려지는 요소당
CAM_SLIDE / CAM_LEAD0.8초 / 0.35초카메라 슬라이드; 샷 큐 전에 시작
PUSH_MAX0.06사진 푸시 한도
WIPE1.2초비포/애프터 와이프
이징: 등장은 ease-out cubic, 슬라이드, 그리기 및 와이프는 ease-in-out cubic.

레이아웃 및 검사

  • 안전 영역 (reel.js의 SAFE): 텍스트, 레이블 및 캡션은 1080x1920 프레임의 x 65–1015, y 269–1248 내에 유지되어야 합니다 (상단 14%, 하단 35%, 측면 6%). 이 수치는 Reels에 대해 널리 사용되는 타사 지침이며, 유기적 게시물에 대한 Meta 발표 규칙이 아닙니다. 앱이 변경되면 게시된 릴의 휴대폰 스크린샷에서 다시 측정하고 safeZone({...})를 설정하세요. 사진과 도면은 이를 넘어갈 수 있지만, 텍스트는 안 됩니다.
  • render.py <block> words.json check build/<block>.check.json은 모든 렌더링 전에 필수입니다. 0.25초마다 샘플링하여 0.5초 이상 지속되는 개별 요소(텍스트, 도면, 칩, 견본, 사진, 캡션) 간의 겹침 및 카메라가 유지되는 동안 안전 영역 밖의 텍스트를 감지하고 실패합니다. 실패 시 레이아웃을 수정하고, 절대 렌더링하지 마세요. 새로운 규칙은 음성 테스트로 증명하세요 (알려진 결함 재도입; 검사가 실패해야 함).
  • 사진에 속하는 텍스트는 사진 요소 내부 (parent: photoDiv)에 scrim() 배경과 함께 배치하거나, 사진을 배경(bg:true)으로 설정합니다. 그렇지 않으면 검사가 텍스트 위 사진 오류를 보고하는데, 이는 의도된 동작입니다.
  • 검사는 프레임 가장자리에서 잘리는 것을 감지하지 못합니다. 자체 검토에서 확인하세요.

블록 구축 (reel.js)

boot(()=>{                                   // 모든 블록은 boot(...)로 시작
  handle('@yourstudio');                     // 안전 영역 상단의 작은 핸들
  $('world').insertAdjacentHTML('beforeend', `<div id="h1" class="abs serif" style="left:90px;top:360px;width:900px;font-size:104px"></div>`);
  hl('h1', 'A small room, *re-planned.*', cue('A small'));          // *...* = 강조 이탤릭; ~word = 타임라인에 없음
  at('#walls', cue('Walls'), null, 'draw', 0.9);                   // fx: 'fade' | 'pop' | 'wipe' | 'draw'
  photo({src:rel('photos/living.jpg'), x:0, y:1920, w:1080, h:1350, a:cue('Light'), push:[1,1.04]});
  beforeAfter({before:rel('photos/before.jpg'), after:rel('photos/after.jpg'), x:90, y:2440, w:900, h:900, a:cue('Before'), t:cue('after')});
  swatch({name:'Oak', note:'floor', color:'#B08A62', x:90, y:4480, size:270, a:cue('Oak')});   // 또는 src: rel('photos/oak.jpg')
  clip({dir:rel('clips/walk'), n:240, x:0, y:5760, w:1080, h:1920, a:cue('Walk'), t0:cue('Walk')}); // frames.py의 프레임
  captions(1120, 3);                         // 타임라인에서 구운 캡션 (음성 릴)
  SHOTS=[[0,540,960,1],[cue('Light'),540,2880,1]];
});

rel(path)는 블록 옆의 파일을 확인합니다. cue('words', after)는 단어가 타임라인에 없으면 오류를 발생시키므로 오타가 확실히 드러납니다. 전체 예제: examples/demo/demo.js.

아랍어 및 기타 오른쪽에서 왼쪽으로 쓰는 릴

  • boot()에서 먼저 rtl()을 호출하세요. 헤드라인은 Noto Naskh Arabic, 텍스트는 IBM Plex Sans Arabic (OFL)을 사용합니다. Latin은 기본 글꼴로 대체됩니다. 아랍어에서는 이탤릭, 자간, 대문자를 사용하지 마세요 (글자가 연결되어야 함). 강조 단어는 색상만 변경합니다.
  • 텍스트 상자는 left + width를 유지합니다. 텍스트는 오른쪽 가장자리에 정렬됩니다. 목록은 오른쪽에서 왼쪽으로 배치하세요 (첫 번째로 명명된 것이 가장 오른쪽).
  • 큐는 발음 구별 부호, 타트윌, 알레프/야/타-마르부타 변형을 무시하고 아랍어-인도 숫자를 변환합니다. 글자가 없는 큐는 오류를 발생시킵니다. ة와 ه는 하나의 글자로 간주되므로 두 단어가 그곳에서만 다른 경우 더 긴 구절을 큐잉하세요. 숫자 단어는 영어에서만 일치합니다. 아랍어에서는 숫자 또는 음역된 단어를 큐잉하세요 (words.py --lang ar는 다국어 모델 선택).
  • 캡션은 ، ؛ ؟ 및 라틴 구두점에서 줄바꿈되며 이를 표시하지 않습니다.
  • 샘플: examples/arabic/.

외관

테마는 engine/templates/reel.css (CSS 변수로 된 색상, 글꼴) 및 글꼴 파일에만 있습니다. 기본 "웜 미니멀": 용지 #F4EFE8, 잉크 #2B2622, 강조 #9C6B4E, Cormorant Garamond 헤드라인 + Jost 텍스트 (둘 다 SIL OFL 1.1). 외관을 변경하려면 변수를 편집하거나, OFL 글꼴을 라이선스 파일과 함께 engine/fonts/에 추가하고 fonts.css 및 window.FONTS (reel.js)에 나열하세요. 소유자에게 그리드의 스크린샷 3-5장을 먼저 요청하고 일치시키세요.

자체 검토 (Claude, 시트나 렌더링을 보내기 전에)

  1. 각 요소는 해당 단어에 맞춰 시작됩니다. 참조선이 먼저 그려집니다.
  2. 단어는 한 번 도착하면 정지; 카메라는 유지되는 동안 정지; 슬라이드 끝에 점프 없음.
  3. 프레임 가장자리에서 잘리는 것 없음; 모든 텍스트가 안전 영역 내에 있음; 하단 35% 아래에 중요한 내용 없음.
  4. 치수는 하나의 축척으로; 모든 숫자에는 단위가 있음; 철자는 승인된 비트 시트와 일치함.
  5. 모든 블록에서 검사 통과(PASS); render.json이 capture: fresh, 모든 블록에 대해 동일한 브라우저를 나타냄.
  6. 결함 검사가 깨끗하거나, 플래그가 지정된 모든 시간을 검사하고 설명함.
  7. assemble.py가 출력한 음량과 실제 피크가 목표값에 도달함.

권리 및 개인정보 보호

  • 소유자의 사진 또는 소유자가 권한을 가진 사진만 사용하세요. 고객의 집: 동의 없이 얼굴, 주소 또는 문서를 포함하지 마세요.
  • 음악: 소유자가 라이선스를 보유한 트랙 또는 게시 시 앱에 추가된 플랫폼 자체 라이브러리만 사용하세요.
  • 소유자가 주장하지 않은 내용(가격, 일정, 수상)을 주장하지 마세요.

문제 해결

  • cue not found: "...": 해당 단어가 words.json에 해당 순서로 없습니다. 비트 시트 또는 큐를 수정하세요.
  • ERR reqfail:file:///...jpg: 사진 또는 클립 경로가 잘못되었습니다. rel()을 사용하고 파일 이름을 확인하세요.
  • 이전 도면의 고스트 복사본 또는 검은 띠: --draft로 렌더링했습니다. 다시 렌더링하세요.
  • 느린 렌더링: 해당 코어 수의 머신에서 --workers 4 (최대 8)를 사용하세요. 확인 및 스토리보드는 빠르지만 비디오는 느립니다.

관련 스킬