코드로 비디오 제작하기
비디오를 순수 함수 render(t)와 단계별 검증 가능한 파이프라인으로 취급합니다. 이 작업 클래스의 거의 모든 실패 모드는 조용히 전달되는 단위/부호/채널/메트릭 버그입니다. 따라서 아래 워크플로우는 각각을 관찰 가능하게 만드는 데 중점을 둡니다.
0. 그리기 전에 계약을 확정하세요
- 단일 시간 소스. 모든 것은
render(t)안에 있습니다. 프레임↔시간 매핑(t = frame/fps)이 전체 공개 API입니다. CSS 애니메이션, 타이머, 프레임 간 상태는 없습니다. 결과: 프레임은 병렬화 가능, 재개 가능, 비교 가능하며 버그는 시간 범위로 국한됩니다. - 결정론. 시드 기반 PRNG(
hash(i)/mulberry32),Math.random금지,Date.now금지. 글꼴과 텍스처를 데이터 URI로 포함시켜 네트워크/글꼴 대체가 중간에 픽셀을 변경할 수 없도록 합니다. - 사양을 주장(assertion)으로 작성하세요. 산문이 아닙니다. 지속 시간, fps, 해상도, 코덱, 픽셀 형식, 음량, 트루 피크, 안전 영역/여백, 금지된 기법, 파일 경로, CLI 플래그, 종료 코드, JSON 출력 형태. 브리프에 숫자가 있으면 반드시 검사에 포함되어야 합니다.
- 이음새 정책을 사전에 결정하세요 — 단발 / 루프 / 연결된 세그먼트. 루프의 경우: 모든 발진기는 정수
k에 대해sin(2π·k·t/D)입니다. 필름은 자신의 첫 번째 상태로 수렴해야 합니다(단순히 "비슷해 보임"이 아님). 이벤트 테이블과 화면의 카운터/레이블은 최종 전환 내에서 재설정됩니다.render(0)와render(D)렌더를 검증하세요. - 프레임당 예산. 콘텐츠를 만들기 전에
render(t)를 측정하세요(1080p 캔버스 드로잉은 2~30ms; 3개의 서브프레임 × 5400프레임은 배수로 증가). 예산을 기록하고 유지하세요. - 인터페이스를 확정한 다음 해시하세요. 캔버스 크기, 페이지/API 표면, 워커 CLI, 출력 경로. 모든 QC는 해당 리비전 해시에 바인딩됩니다 — 오래된 해시에 대해 QC된 렌더는 가치가 없습니다.
- 의도적인 편차를 기록하세요. 현실이 브리프와 다른 모든 부분(깔끔한 선의 그리드 흔들림, 명시된 최소 크기보다 작은 마이크로 텍스트, 명시된 범위 밖의 스프링 레이트)은 이유와 함께 짧은 목록에 기록되어 검토자가 발견하기보다 평가할 수 있도록 합니다.
1. 첫날 세 가지 프로브
| 프로브 | 주장 | 이유 |
|---|---|---|
| 캡처 무결성 | 정확한 픽셀, 알파 == 255, 색상 범위, 첫/마지막 프레임 존재 | 반투명 누적, 범위/색공간 오류가 조용히 전체 필름을 망침 |
| 비용 | 장면별 render(t) ms 분포, 메모리 | 아키텍처 변경은 지금은 저렴하고 나중에는 비쌈 |
| 모션 | 전체 지속 시간에 대한 프레임 간 차이 시리즈 + 가장 긴 정지 실행 | 죽은 시간 범위를 즉시 보여줌 |
실제 실패에서 나온 규칙:
- 흰색 합성 함정. 알파를 흰색 위에 합성하는 뷰어로 프레임을 검토하면 반투명이 숨겨집니다. 인코딩은 검은색 위에 합성합니다. 검토 경로가 전달 경로와 정확히 동일하게 합성되도록 하고 두 경로 간의 픽셀을 비교하세요.
- 메트릭이 무엇을 볼 수 있는지 아세요. 12배 다운스케일 후 얇은 선의 서브픽셀 모션은 평균 절대 차이를 ~0.03/255만큼 변경하는 반면, 전체 프레임 밝기 변화는 ~1.5/255만큼 움직입니다. 허용 메트릭이 다운스케일된 평균인 경우, 가장 저렴한 실제 레버는 더 큰 흔들림이 아닌 전역 값 채널(예: 비트 속도에서 2Hz ± 3% 루미넌스 펄스)입니다.
- 에셋보다 메트릭을 먼저 검증하세요. 검사가 실패하면 먼저 원시 신호(1ms에서의 엔벨로프, 픽셀 행 프로파일, 프레임 시리즈)를 덤프하세요. "에셋이 잘못됨"의 절반은 탐지기가 잘못된 것입니다.
- 탐지기를 자체 설명 가능하게 만드세요. 시스템적 편향(필터 그룹 지연, 인과 분석 창 지연)을 모델링하고 보상하세요. 가장 큰 후보보다 이벤트에 가장 가까운 후보를 선호하세요. 통과할 때까지 임계값을 조정하지 마세요.
- 정확 vs 근사 주장. "render(0) ≡ render(D), PSNR ∞"는 증명입니다. "첫 번째와 마지막 인코딩된 프레임이 2/255만큼 다름(한 프레임의 모션)"은 관찰입니다. 보고서에서 둘을 혼동하지 마세요.
번들: scripts/av_audit.mjs는 컨테이너/모션/루프/음량/시트 절반을 구현합니다(ffmpeg 전용, npm 의존성 없음).
2. 리뷰 루프는 기능이지 오버헤드가 아닙니다
- 그리드화: 수백 장의 스틸 → 한 번에 검토되는 여러 장의 연속사진.
- 가이드 포함: 안전 영역 상자, 중앙 십자선, 타임코드, 이벤트 ID. 검토자가 레이아웃을 기억할 필요가 없어야 합니다.
- 고정 간격이 아닌 이벤트별로 샘플링: 각 의도된 이벤트에서 한 프레임, 각 이동 중간에서 한 프레임.
- 이음새를 가장 집중적으로 살피세요: 장면 전환, 루프 랩, 세그먼트 경계, 첫/마지막 프레임.
- 의심스러운 요소를 빈 캔버스에 4~6배 확대/격리하세요. 대부분의 버그는 전체 프레임에서 보이지 않습니다.
- 아티팩트는 일회용입니다: 하나의 명령으로 재생성됩니다. 리뷰 프레임을 수동으로 패치하지 마세요.
- 이슈 목록을 유지하고(어수선함 / 박자 맞지 않음 / 겹침 / 안전 영역 밖 / 읽을 수 없음 / 정지) 각 수정 후 다시 검토하세요.
3. 모션 및 루프 제작
- 주변 움직임 ≠ 액션. 모든 장면에는 액션과 다른 주파수의 연속 채널(드리프트, 웨이브, 유휴 회전, 값 펄스)이 필요하며, 영점 교차점이 비트에 정렬되어 액션이 여전히 깔끔하게 떨어지도록 합니다. 이것이 "아무것도 멈춰 있지 않음"을 만족시키는 것입니다.
- 하나의 이징 패밀리. 임계 감쇠 스프링
1-(1+x)e^-x(오버슈트 없음)은 이동용; 지수 이징 아웃/인은 와이프, 누름, 종료용. 착지를 이벤트와 일치시키세요: 시작 = 이벤트 − 지속 시간, 이벤트에서 정확히 progress = 1이 되도록 정규화하세요. - 두 곳의 모션 블러: 프레임 함수 내부(2~4개의 셔터 샘플을 이동 평균 —
alpha = 1/(k+1)— 으로 평균내어 완전히 불투명하게 끝남), 이벤트 주변에서 더 조밀하게; 인코더 측 시간 블렌드가 더 있습니다. 문서가 아닌 인덱스 코딩된 픽스처(픽셀이 프레임 번호를 인코딩하는 프레임을 공급하고 어떤 인덱스가 평균화되었는지 읽음)로 필터 창 의미를 증명하세요. - 구성에 의한 연속성. 장면 전반에 걸쳐 하나의 지오메트리/상태 모델을 유지하고 절단이나 디졸브 대신 변환(모핑, 재질 교체, 하드 에지 와이프 플러드, 스캔 라인)하세요.
- 길이 유지 모핑: 모양을 각도/호 길이로 매개변수화하여 블렌드가 배율 조정 대신 성장하도록 합니다.
- 기본 금지 목록(요청 시에만 삭제): 입자, 글로우, 렌즈 플레어, 카메라 흔들림, RGB 분할, 크로스페이드, 일반 그래디언트, 바운시 오버슈트.
4. 렌더링 및 인코딩
- 형식 vs 목표: 먼저 처리량을 측정하세요. 무손실 PNG 프레임은 수 MB이며 파이프에 부담을 줍니다. 고품질 JPEG는 약 1.5배 빠릅니다. 한 번 결정하고 유지하세요.
- 파이프, 스풀 금지: 프레임을 ffmpeg stdin에 공급하세요(
-f image2pipe -framerate <fps*sub> -vcodec png -i -).tmix로 블렌드, 그룹의 마지막 프레임 선택, 재타이밍,-frames:v <n>으로 트림. - 프레임 수가 아닌 비용으로 병렬화하세요. 장면별 ms를 측정한 다음 대략 동일한 시간의 청크로 분할하세요. 균일한 프레임 수는 가장 느린 워커가 벽시계를 설정하게 만듭니다.
- 세그먼트는 concat 안전해야 합니다: 각 끝에 2개의 경계 프레임 반복, 동일한 인코더 매개변수 및 시간 기준, ffprobe로 프레임/지속 시간 검증, 그런 다음
-c copyconcat. - 파이프라인을 단계화하여 실패 시 한 단계만 비용이 들도록 하세요: 렌더 → 블렌드 → 인코딩 → 오디오 → 먹싱 → 측정. 각 단계는 파일을 씁니다.
- 단계 간 무손실 비디오 마스터 유지(모든 곳에서
-c copy), 음량 처리는 마지막에 하세요. - 알려지지 않은 툴체인을 프로브하세요 의존하기 전에: 필터/인코더 목록, 브라우저 실행 파일, 버전;
$FFMPEG/$FFPROBE재정의 허용; PATH 추측보다 고정된 로컬 바이너리 선호. 브라우저와 함께 제공되는 축소된 "media" ffmpeg는 일반적으로x264/tmix/loudnorm이 누락되어 있습니다. - 자식 출력을 파이프가 아닌 파일 디스크립터를 통해 캡처하세요. 제한된 샌드박스 내에서
spawn/execSyncwithstdio: 'pipe'는 동일한 바이너리가 셸에서 잘 실행되는 동안EPERM으로 실패할 수 있습니다. 대신 열린 파일 핸들을 전달하세요:spawnSync(cmd, a, { stdio: ['ignore', fdOut, fdErr] }), 그런 다음 파일을 읽으세요 — 순수 존재/버전 프로브에는stdio: 'ignore'를 사용하세요. stdout과 stderr를 별도 파일로 라우팅하여 바이너리 프레임 데이터가 로그 텍스트와 섞이지 않도록 하세요. - 소스에서 엔드투엔드 재빌드로 끝내세요. 전달된 파일이 운 좋은 캐시가 아님을 보장합니다.
5. 오디오 및 먹싱
- 픽쳐와 동일한 시간 기준 및 동일한 결정론(오프라인 렌더, 시드, 실시간 의존성 없음).
- 모든 가청/가시 이벤트에 대한 단일 진실 공급원으로 하나의 큐 테이블
{t, name}; SFX는 자체 버스에 있으며 음악보다 조용합니다. - 2패스 loudnorm으로 정규화(
print_format=json→ measured_* →linear=true로 두 번째 패스), 그런 다음 독립적인 세 번째 측정으로 검증하세요. 통합 음량이 ±0.5 LU 이내이고 트루 피크가 목표 이하인지 주장하세요. - 루프: 테일(~40ms)을 정확히 0으로 페이드하고, 첫 번째 샘플이 클릭하지 않도록 하세요. 그리드에 대한 타악기 히트 정렬을 측정하고 보고하세요.
- 먹싱
-c:v copy -c:a aac -b:a <spec>, 그런 다음 ffprobe로 컨테이너를 "보기"가 아닌 주장으로 프로브하세요. -b:a Nk는 ffmpeg의 네이티브 AAC 인코더에 대한 목표지, 최소값이 아닙니다(조용한 소재에서 320k 요청에 대해 약 180~250kbps 측정됨;-aac_coder twoloop가 도움). 선언된bit_rate를 다시 읽고 요청된 숫자를 반복하는 대신 측정된 평균을 보고하고, 사양이 비트레이트를 요구할 때 명시적으로 언급하세요.
번들: scripts/mux_normalize.mjs.
6. 드로잉 레이어를 저렴하고 결정론적으로 유지하세요
- 시간 불변인 모든 것을 캐시하세요: 구운 배경, 텍스트 마스크, 패턴, Path2D. 캐시 키는 픽셀을 변경할 수 있는 모든 매개변수를 포함합니다.
- 캔버스 풀은 소스 캔버스 차원에서 크기를 조정해야 하며 논리적 콘텐츠 크기에서가 아닙니다. 그렇지 않으면
drawImage가 조용히 늘리거나 자릅니다(이로 인해 잘리거나 손상된 유형이 생성됨). - 프레임 루프에서
shadowBlur/filter를 피하고 지오메트리를 선호하세요. - 메모리: 1080p 캔버스는 약 8MB. 풀을 제한하세요.
- 서브프레임 수가 프레임 예산을 선형으로 곱한다는 것을 기억하세요.
7. 협업
- 작성자와 검증자를 분리하세요. 검증자는 QC 스크립트와 납품물을 실패시킬 권한을 소유하며, 작성자가 확인하지 않은 차원(알파, 페이지 대비 충실도, 음량, 루프 폐쇄)을 독립적으로 확인합니다. 이것이 작성자가 눈치채지 못하는 버그 클래스를 잡는 방법입니다.
- 분리된 쓰기 범위 + 고정된 인터페이스로 병렬 작업이 충돌하지 않도록 하세요. 리비전을 해시하고 변경 후 QC를 다시 실행하세요.
- 크게 실패하세요: 0이 아닌 종료, 주장당 하나의
PASS/FAIL라인, 기계 판독용 JSON, 조용한 폴백 없음.
8. 안티패턴
- 흰색 합성된 리뷰 이미지로 색상 판단.
- 특성을 파악하지 않은 메트릭을 충족시키기 위해 에셋 편집.
- 모델을 수정하는 대신 탐지기의 임계값 조정.
- 20초 페이지 측 프로브가 답하는 질문에 전체 렌더 재실행.
- 비용이 크게 다른 장면에 걸쳐 균일한 프레임 수 청킹.
- 전달 경로와 다른 리뷰 경로(스케일, 색상 범위, 합성).
- "괜찮아 보였음" 대신 주장; 0으로 종료되는 실패.
- 비ASCII를 망가뜨리는 도구로 텍스트/에셋 수술(편집 후 인코딩 확인).
- 소스를 수정하는 대신 리뷰 프레임을 수동으로 수정.
- 크로스페이드 뒤에 상태 불연속성 숨기기.
9. 체크리스트
사전 비행 — 사양 → 주장; render(t) 서명 고정; 이음새 정책 + 루프 대수; 에셋 포함; 세 가지 프로브 존재; 프레임당 예산 설정; FFMPEG/FFPROBE 확인 및 기능 프로브 완료.
진행 중 — 각 장면 후: 비용 프로브, 모션 시리즈, 이벤트 샘플링 리뷰 그리드; 이슈 목록 마감; 구문/타입 검사 + 편집된 소스에 대한 비ASCII 검사; 리비전 해시.
사전 납품 — 소스에서 재빌드; 전체 렌더(비용 가중치, 경계 프레임, concat 검증); 음량 2패스 + 세 번째 검증; 독립 측정 스크립트 → PASS/FAIL; 정확 vs 근사 주장을 명시적으로 보고; 임시 스크립트/스크린샷 삭제 및 각각에 대한 한 명령 재생성 문서화.
완료 정의 — 컨테이너 메타데이터 정확; 이음새 동작 증명됨; 정지 실행 한계 충족; 음량/트루 피크 충족; 화면 텍스트가 브리프와 동일; 한 명령으로 소스에서 재빌드됨; 고정된 해시에서 모든 QC 통과.
10. 번들 스크립트
둘 다 단일 파일, 제로 의존성(ffmpeg/ffprobe만), $FFMPEG/$FFPROBE → PATH → 일반 위치에서 도구를 확인하고, 주장당 하나의 PASS/FAIL 라인을 출력하고, --json으로 JSON을 내보내며, 실패 시 0이 아닌 값으로 종료합니다. 이 스킬의 자체 경로에서 디렉토리를 확인합니다.
# 완성된 파일 QC: 컨테이너 대 기대치, 정지 실행 한계, 루프 이음새,
# 음량/트루 피크, 인간 검토를 위한 선택적 연속사진
node av_audit.mjs film.mp4 --expect-duration 30 --expect-fps 60 --expect-size 1920x1080 \
--static-limit 0.5 --sheets ./sheets # exit 0 = 모든 검사 통과
# WAV를 목표로 정규화하고 인코딩된 비디오에 먹싱(비디오 스트림 복사),
# 그런 다음 세 번째 독립 측정 패스로 검증
node mux_normalize.mjs silent.mp4 score.wav film.mp4 --lufs -14 --tp -1 --abr 320k
av_audit.mjs는 의도적으로 저해상도(160×90) MAD 시리즈를 사용합니다: 전체 프레임 값 변경과 블록 이동을 보지만 얇은 선의 서브픽셀 모션은 보지 않습니다 — 섹션 1의 메트릭 감도 규칙과 쌍을 이루세요. 루프의 경우 first_vs_last_psnr_db를 한 프레임 모션에 대한 관찰로 취급하고 렌더러 자체의 두 렌더를 비교하여 별도로 정확성을 증명하세요.
한 줄 요약 — 성공은 세 가지에 달려 있으며, 그중 어느 것도 드로잉 기술이 아닙니다: 시간 함수가 순수하고 검증 가능하며, 메트릭이 최적화되기 전에 이해되고, 리뷰 및 전달 경로가 동일한 경로입니다.