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로 제공되며, 덮어쓰지 않음
게이트 (절대 건너뛰거나 순서를 변경하지 않음; 각 ⛔에서 소유자의 승인을 위해 중단)
- 브리프. 릴당 하나의 아이디어 (방 공개, 소재 이야기, 평면도 변경, 팁). 사진과 핸들을 요청하세요. 각 사진이 보여주는 내용을 나열하세요. 처음 2초 안에 시청자를 사로잡으세요.
- 비트 시트 (
beats.txt): 화면에 표시될 정확한 단어들을 순서대로, 한 줄에 하나의 비트로 작성합니다. 비트는 짧게 유지하세요 (2–7단어). 목록의 각 항목은 자체 줄에 배치하여 각각이 충분히 머물 시간을 갖도록 하세요. ⛔ 소유자가 단어를 승인합니다. 승인 후 단어 변경은 블록 타이밍을 다시 조정해야 함을 의미합니다 (음성이 있는 경우 음성도 다시 녹음). 변경 전에 이를 반드시告知하세요. - 타임라인.
- 음성 없음:
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실행 후, 대본과 자막을 단어 단위로 비교합니다. 그림은 테이크에 맞춰지며, 그 반대는 절대 안 됩니다.
- 음성 없음:
- ⛔ 스토리보드 스틸. 블록 레이아웃을 구축하고 (위치만으로 충분),
check를 실행한 후render.py A.js words.json storyboard build/sb및storyboard_sheet.py build/storyboard.png build/sb를 실행합니다. 시트를 자체 검토하고 (아래 목록 참조) 보냅니다. 소유자가 레이아웃을 승인하기 전까지는 아무것도 애니메이션되지 않습니다. - 모션 구축. 모든 요소를 해당 요소를 명명하는 단어에 맞춰 큐잉합니다 (아래 규칙 참조). 모든 변경 후
check를 실행합니다. - 렌더링. 블록당
render.py A.js words.json video build/A.mp4 --workers 4(보내는 어떤 것에도--draft사용 금지). 모든 블록의build/A.mp4.render.json은"capture": "fresh"를 나타내고 다른 블록과 동일한 브라우저를 사용해야 합니다. 브라우저가 변경된 경우 모든 블록을 다시 렌더링하세요. - 결함 검사. 모든 블록에 대해
faultscan.py build/A.mp4 --sheet build/A_contact.png를 실행하고, 최종 릴에 대해서도 실행합니다. 플래그가 지정된 모든 시간을 확인하세요. 검은 띠나 1프레임 플래시는 결함입니다. 중단하고 수정하세요. 어두운 사진 콘텐츠는 괜찮습니다. - 조립.
assemble.py blocks.json <Reel>_REV01_<date>.mp4 [--music bed.mp3]를 실행합니다. 측정된 음량을 출력합니다. 대상값(-14 LUFS 기본값)에 도달해야 하며, 실제 피크는 -1.5 dBTP 이하여야 합니다. - ⛔ 검토. 릴과 그 연락처 시트를 보냅니다. 사진만 수정하는 경우 변경된 블록만 다시 렌더링하고 다음 _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_LEAD | 0.28초 / 0.08초 | 단어 페이드 인; 단어 바로 직전에 시작 |
| STREAM | 0.10초 | 스트림 모드 헤드라인 단어 간격 |
| REVEAL / EXIT | 0.5초 / 0.4초 | 블록, 칩, 견본, 사진 등장/퇴장 |
draw (at(...,'draw',dur)) | 0.6–1.0초 | 그려지는 요소당 |
| CAM_SLIDE / CAM_LEAD | 0.8초 / 0.35초 | 카메라 슬라이드; 샷 큐 전에 시작 |
| PUSH_MAX | 0.06 | 사진 푸시 한도 |
| WIPE | 1.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, 시트나 렌더링을 보내기 전에)
- 각 요소는 해당 단어에 맞춰 시작됩니다. 참조선이 먼저 그려집니다.
- 단어는 한 번 도착하면 정지; 카메라는 유지되는 동안 정지; 슬라이드 끝에 점프 없음.
- 프레임 가장자리에서 잘리는 것 없음; 모든 텍스트가 안전 영역 내에 있음; 하단 35% 아래에 중요한 내용 없음.
- 치수는 하나의 축척으로; 모든 숫자에는 단위가 있음; 철자는 승인된 비트 시트와 일치함.
- 모든 블록에서 검사 통과(PASS);
render.json이capture: fresh, 모든 블록에 대해 동일한 브라우저를 나타냄. - 결함 검사가 깨끗하거나, 플래그가 지정된 모든 시간을 검사하고 설명함.
assemble.py가 출력한 음량과 실제 피크가 목표값에 도달함.
권리 및 개인정보 보호
- 소유자의 사진 또는 소유자가 권한을 가진 사진만 사용하세요. 고객의 집: 동의 없이 얼굴, 주소 또는 문서를 포함하지 마세요.
- 음악: 소유자가 라이선스를 보유한 트랙 또는 게시 시 앱에 추가된 플랫폼 자체 라이브러리만 사용하세요.
- 소유자가 주장하지 않은 내용(가격, 일정, 수상)을 주장하지 마세요.
문제 해결
cue not found: "...": 해당 단어가words.json에 해당 순서로 없습니다. 비트 시트 또는 큐를 수정하세요.ERR reqfail:file:///...jpg: 사진 또는 클립 경로가 잘못되었습니다.rel()을 사용하고 파일 이름을 확인하세요.- 이전 도면의 고스트 복사본 또는 검은 띠:
--draft로 렌더링했습니다. 다시 렌더링하세요. - 느린 렌더링: 해당 코어 수의 머신에서
--workers 4(최대 8)를 사용하세요. 확인 및 스토리보드는 빠르지만 비디오는 느립니다.