film-engine: 코드로 그리는 영화, 한 프레임씩
키트 위치: 이 스킬의 모든 명령은
reels-maker폴더(github.com/thativanng/reels-maker의 클론으로,template/와tools/를 포함)에서 실행하세요. 프로젝트에 이 폴더가 없으면 먼저 해당 레포지토리의SETUP.md를 따라 설정하세요.
한 문단으로 보는 아이디어
여기서 영화는 하나의 HTML 페이지이며, 규칙은 단 하나입니다: window.seek(t) 는 시간 t에 해당하는 프레임을 항상 정확히 그려야 하며, t 외에는 아무것도 사용하지 않습니다. 화면 속 어떤 것도 실제 시계를 읽어서는 안 됩니다. 그런 다음 렌더러가 30fps로 t를 진행하며 각 프레임을 2배 해상도로 스크린샷하고 ffmpeg로 전달합니다. 페이지가 결정론적이기 때문에, 재렌더링 시 결과가 일치하며(평균적으로 약간의 레벨 차이 이내, GPU 노이즈로 인해 단일 픽셀이 최대 60레벨까지 변동 가능), 17.3초에서의 정지 이미지는 비디오에서 보여줄 프레임과 동일하며, 모든 검사(film-audit)는 영화를 재생하지 않고도 어떤 순간이든 확인할 수 있습니다.
과정 (단계를 건너뛰지 마세요)
- 먼저 브리핑과 룩을 확정하세요. reel-intake는 서명된 브리핑과 스토리보드(형식, 길이, 사운드, 절대 등장하지 않아야 할 것)로 마무리됩니다. reel-style은 선택된 룩으로 마무리됩니다. 이 스킬의 빌드 단계를 열기 전에 두 가지가 모두 준비되어 있어야 합니다.
- 스토리보드. reel-intake가 작성합니다 (시간 · 화면에 표시될 텍스트 · 사운드 및 자막 · 이유; references/brief.md), 그리고 빌드 전에 사람이 서명합니다. 배너가 있는 스타일은 배너 열을 추가합니다.
- 테스트 스트립. 선택된 룩으로 처음 약 10초를 빌드하고(걷기 스타일 영화는 한 챕터를 보여주기 위해 15~30초가 필요할 수 있음), 정지 이미지를 캡처하여 승인을 받으세요. 서명되지 않은 룩으로 전체 빌드하는 것은 낭비입니다.
- 전체 영화를 빌드하세요 (스타일 팩이 장면을 빌드하는 방법을 알려줍니다).
- 게이트 (film-audit): 40fps 충돌 검사, 픽셀 프로브, 그리고 독립 감사자. 이 과정 전까지는 아무것도 사람에게 보여주지 마세요.
- 렌더링:
node tools/render.mjs --page reels/<name>/page.html --out reels/<name>/out/film-silent.mp4(2배 프레임, lanczos로 축소, 컬러 태그 없음). - 오디오:
python3 template/audio.py out/film.wav(영화와 동일한 시계를 따르는 큐 목록). - 마무리:
zsh tools/mux.sh out/film-silent.mp4 out/film.wav out/film.mp4. 이 명령은 −14 LUFS, 리미터, 태그 없는 H.264 파일 및 영화와 같은 비율의 전화 미리보기(짧은 변 720px)를 생성합니다. - 커버:
node tools/cover.mjs --page reels/<name>/page.html --out reels/<name>/out/cover.jpg. 커버 모드가 있는 페이지(스타터의?cover, 설정의cover)는 디자인된 정지 이미지를 그리고, 다른 페이지는T=시점의 프레임을 제공합니다. 영화 자체 비율로 저장되며 2MB 미만으로 유지되고, 9:16 영화의 경우 중앙 3:4 크롭(인스타그램 그리드용, 보고됨)도 함께 제공됩니다. LinkedIn은 게시 시에만 커버를 설정할 수 있습니다: 게시 후에는 게시글 텍스트만 수정 가능합니다(LinkedIn 도움말, "공유된 게시물 편집", https://www.linkedin.com/help/linkedin/answer/a522811). LinkedIn의 썸네일 사양(JPG 또는 PNG, 비디오와 동일한 비율, 최대 2MB)은 검색 요약에 인용된 LinkedIn 도움말에서 가져온 것입니다. 페이지 자체는 로드되지 않으므로, 확인되지 않은 정보로 취급하세요. - 먼저 전화 미리보기를 보내고, 그다음 전체 파일과 커버를 보내세요.
페이지 규약
window.seek(T): 프레임 T를 그립니다. 이것이 애니메이션을 처리하는 유일한 곳입니다.window.ready: 폰트, 텍스처 및 첫 번째 프레임이 로드된 후 해결되는 프로미스입니다.window.DUR: 영화의 길이(초)입니다.window.FILM = { w, h, format, safe }: 프레임 크기와 안전 영역(픽셀 단위). 모든 도구가 이 값을 기준으로 창 크기를 조정하므로, 페이지가 자체 포맷을 소유합니다.- 선택 사항:
window.cover()는 프레임 대신 커버를 그리고,FILM.coverAt은 커버가 해당하는 시간을 지정합니다 (tools/cover.mjs는 둘 다 사용합니다; 없으면T=시점의 프레임을 캡처합니다). window.collide(T)및window._dbg: film-audit을 위한 훅 (해당 스킬 참조).- 도구들은 키트 폴더를 http로 서빙합니다 (ES 모듈은
file://에서 로드되지 않음). 브라우저에서 페이지를 실시간으로 보려면npm run serve를 실행하고 http://127.0.0.1:8765/template/starter.html 을 여세요.
포맷, 스타터 및 다른 언어
template/formats.json은 네 가지 크기(9:16 1080×1920 · 4:5 1080×1350 · 1:1 1080×1080 · 16:9 1920×1080), 플랫폼→포맷 매핑, 각 플랫폼의 안전 영역과 출처를 포함합니다. 브리핑의 포맷 라인이 이 중에서 선택합니다.- 모든 릴은 자체 폴더
reels/<name>/에 저장됩니다 (gitignore됨): 브리핑, 스토리보드,page.html(스타터 또는 film.html의 복사본),reel.json또는career.json,out/. 페이지는 키트 루트에서 폰트와 formats.json을 로드하므로, 복사본이 거기서 작동합니다. 도구를 해당 위치로 지정하세요:PAGE=reels/<name>/page.html. template/starter.html+template/reel.json은 voxel walk를 제외한 모든 룩의 골격입니다. 단어와 타이밍은 설정 파일에 있으며,?fmt=또는 설정에 따라 크기가 조정되고, 모든 단어는 플랫폼의 안전 영역 내에 유지됩니다.?guides는 체크 전용으로 덮힌 영역을 빨간색으로 표시합니다. 절대 켠 상태로 렌더링하지 마세요. 이것은 골격일 뿐, 룩이 아닙니다: reel-style이 토큰과 레이아웃을 다시 작성합니다.template/film.html(복셀 템플릿) 은 4:5 전용이며, 안전 영역(safe는 0)을 사용하지 않습니다: 배너와 자막이 의도적으로 가장자리 근처에 위치하므로safe프로브는 항상 통과하고 감사자의 눈이 검사 역할을 합니다. 카메라는 4:5에 맞춰져 있습니다. 다른 비율을 사용하려면 카메라를 다시 설정하고 모든 게이트를 다시 실행해야 합니다.- 중국어, 일본어, 한국어: 스타터는 렌더링 시 Google Fonts에서 Noto Sans TC를 로드하고(설정에서 사용하는 문자만), CJK 텍스트를 라틴 문자 크기의 약 0.72배로 렌더링합니다(CJK 문자는 전체 em 너비). 줄바꿈이 필요한 곳에
\n을 작성하세요: 중국어는 어떤 두 문자 사이에서도 줄바꿈이 가능하므로, 긴 줄에는 선택한 줄바꿈이 필요합니다.template/reel.zh.json은 작업 예제입니다. 다른 스크립트는 스택에 자체 폰트가 필요합니다.
시계 및 타이밍
- 하나의 영화 시계가 장면, 카메라, 배너, 자막 및 사운드를 구동합니다. 나중에 모든 타이밍을 다시 조정하지 않고 비트를 삽입하려면 리맵을 사용하세요: 장면 시계는 삽입 지점에서 멈추고 오버레이는 실제 시계를 유지합니다:
오디오 스크립트도 동일한 테이블이 필요합니다: 장면 시간에 삽입된 길이를 더하는const INS = [[24.0, 5.0, 'detail']]; // 장면 시간 24.0에서 5초 분량의 'detail' 비트를 재생 function remap(T) { let sh = 0; for (const [a, len, id] of INS) { if (T < a + sh) return { t: T - sh, ins: null }; if (T < a + sh + len) return { t: a, ins: id, u: T - sh - a, len }; sh += len; } return { t: T - sh, ins: null }; }RT(t)를 작성하고, 모든 큐를RT(t)에 배치하세요. 그렇지 않으면 삽입 이후의 모든 큐가 밀려납니다. (템플릿에는 삽입이 없으므로 audio.py에도 필요하지 않습니다.) - 이징: 카메라 움직임에는
eio, 물체가 들어올 때는eo, 걷기 및 운전에는trap(사다리꼴 속도)을 사용하여 움직이는 물체가 끝에서 "딱" 끊기지 않도록 하세요. - 카메라 키는 절대 위치
A(pos, target)또는 캐릭터의 바닥 지점을 기준으로 한F(offset, targetOffset)입니다. 키 사이를eio로 블렌딩하세요. 중요한 비트에서는 고정하고, 항상 움직이는 샷은 불안정해 보입니다.
오버레이 (DOM에, 캔버스 위에)
- 배너: 연도, 회사, 역할 필(role pill), 그리고 현재 연도에 점이 숨쉬는 타임라인. 배너는 새 장면이 화면에 나타날 때 변경됩니다(세트가 올라오고, 의상이 바뀌고, 주인공이 도착했을 때). 코드가 장면을 구축하기 시작할 때가 아닙니다. 매 변경 시점 전후 ±1초에서 프레임 시트로 확인하세요.
- 자막: 비트당 한 줄의 스토리, 핵심 구문은 굵은 악센트.
text-wrap: balance를 사용하세요. 독자에게 초당 약 3단어를 제공하고, 빠른 카메라 움직임이 그 뒤의 피사체를 드러내기 전에 자막을 페이드 아웃하세요. - 말풍선: 캐릭터를 따라다니며, 머리 포인트를 투영하고 말풍선을 프레임 안쪽 최소 24px 이상 유지하세요.
- 엔드 카드: 이름, 태그라인 하나, 잘하는 것, 인용구 하나. 뒤의 장면은 흐려집니다.
오디오
- 큐는 영화 시계에 맞춰, 해당 순간 옆에 기록됩니다(걸음 수당 스텝 소리, 사물이 나타날 때 차임, 착지할 때 쿵 소리).
- 전환에 윙(whoosh) 소리를 사용하지 마세요. 싸구려 느낌입니다. 대신 차임, 틱, 부드러운 충격음을 사용하세요.
template/audio.py의 기본 배경음은 사인파로 생성되므로 어디서든 무료로 사용할 수 있습니다. 누군가 AI 생성 트랙을 가져온 경우 라이선스를 확인하세요: 일부 모델의 가중치는 비상업적 용도만 허용합니다.- 음량: −14 LUFS로 2패스 loudnorm (측정 후 선형 적용), 그 다음 리미터. 1패스는 초과할 수 있습니다.
우리가 며칠을 소비하게 만든 렌더링 사실들 (자세한 내용은 references/gotchas.md)
- 2배 해상도로 렌더링하고 축소하세요. EffectComposer가 캔버스 안티앨리어싱을 제거하므로 1x 렌더링은 모든 곳에서 계단 현상이 발생합니다.
- MP4에 색상 태그를 넣지 마세요. JPEG 파이프가 전체 범위로 태그하여 일부 플레이어에서 색상이 변할 수 있습니다. 필터에
setparams=...=unknown을 사용하세요. - 하나의 평면에 있는 두 개의 얼굴이 깜빡입니다 (z-파이팅). 앞쪽 얼굴에 깊이 오프셋을 주세요.
- 폰트는 캔버스 텍스트를 그리기 전에 로드되어야 합니다 (
document.fonts.load). 그렇지 않으면 조용히 기본 폰트로 대체됩니다. - 헤드리스 Chromium에서 GPU를 켜세요 (macOS에서는
--use-angle=metal). GPU가 없으면 렌더링 속도가 약 10배 느려집니다.
모든 단계에서 프롬프트 없이 작업하기
사용자는 키트의 허용 규칙에 한 번 동의했습니다 (SETUP.md 4단계). 그 규칙 내에서 작업하세요. 그렇지 않으면 매번 승인을 요청받게 됩니다:
- 파일 쓰기 및 변경은 Write 및 Edit 도구를 사용하세요: 페이지, reel.json, 브리핑, 스토리보드, AUDIT.md. Python이나 쉘 스크립트, heredoc,
sed -i로 생성하지 마세요. 각각은 승인 프롬프트입니다. - 측정은 키트의 도구를 사용하세요 (stills, probe, sheet, collide, render, cover). 일회성
python3 -c스니펫을 사용하지 마세요. mkdir, 루프, 체인은 필요 없습니다: 모든 도구는 자체 출력 폴더를 생성하고, 한 번의 호출로 시간 목록을 받습니다 (stills.mjs … 1.5,6,12).- 릴의 자체 스크립트 (사운드 스크립트, 헬퍼)는
reels/<name>/에 저장되며python3 reels/<name>/audio.py또는node reels/<name>/x.mjs로 실행되며, 이는 규칙에 포함됩니다.
명령어
각 도구를 하나의 평문 명령어로 실행하세요 (플래그, VAR=… 앞에 붙이지 않음, 루프 없음, && 체인 없음). 그러면 사용자의 허용 규칙(node tools/*)이 이를 포함하여 모든 단계를 승인하라고 요구하지 않습니다. 환경 변수는 여전히 스크립트에서 작동합니다.
npm run doctor # 이 머신을 먼저 확인
npm run demo # 12초 데모: out/demo/
node tools/stills.mjs "2,8.5,15" # 2배 스틸 → out/stills (복셀 템플릿)
node tools/stills.mjs --page template/starter.html --format 16:9 1.6 # 다른 비율의 스타터
node tools/stills.mjs --page template/starter.html --cfg reel.zh.json 1.6 # 다른 설정 (여기서는 중국어)
node tools/stills.mjs --dsf 1 --out out/chk 2,8.5 # 시트용 빠른 1x 프레임
python3 tools/sheet.py out/chk/sheet.jpg 6 300 out/chk/t*.jpg
node tools/render.mjs --page reels/<name>/page.html --out reels/<name>/out/film-silent.mp4
python3 template/audio.py out/film.wav
zsh tools/mux.sh out/film-silent.mp4 out/film.wav out/film.mp4