Communitygithub.com

html-to-video

HTML/Canvas 애니메이션을 프레임별로 렌더링하여 MP4로 변환합니다 (배경 음악, GIF, 투명 레이어 출력 지원). 「코드로 영상 만들기」「HTML → 동영상」「MP4/GIF 내보내기」「동적 다이어그램/홍보 영상/데이터 애니메이션 제작」「프레임별 렌더링」과 같은 요청에 사용됩니다. 자체 렌더러와 11개의 즉시 사용 가능한 템플릿(멀티 쇼트 설명 영상, 세로 9:16, 전후 비교, 플로우차트, 자막 카드), 아이콘/캐릭터/카메라 움직임 에셋 라이브러리를 제공하며, 투명 출력, 외부 자막 타임라인, JSON 쇼트 시트를 지원합니다. 의존성 설치 후 한 줄 명령으로 영상을 생성합니다.

html-to-video란 무엇인가요?

html-to-video는 HTML, Canvas, CSS, SVG로 작성된 애니메이션을 프레임별로 렌더링하여 H.264 MP4 동영상으로 변환하는 코드 기반 영상 제작 도구입니다. AI 영상 생성과 달리 완전한 결정론적(동일 입력 → 동일 출력)이며, 프레임 단위의 정밀한 제어, 완벽한 한글 텍스트 렌더링, 추가 비용이 없는 것이 특징입니다. 설명 애니메이션, 동적 다이어그램, 제품 데모, 데이터 시각화, 홍보 영상 등에 최적화되어 있습니다. 이 도구는 Headless Chromium을 사용하여 각 프레임을 캡처한 후 FFmpeg로 인코딩하며, 11개의 사전 제작된 템플릿(설명 영상, 타이틀 카드, 막대/꺾은선 차트, 숫자 카운터, 타임라인, 전후 비교, 플로우차트, 자막 카드, 세로형 9:16)을 제공합니다. 템플릿은 단일 파일이며 상단의 CFG 설정 블록만 수정하면 됩니다. 주요 기능으로 투명 배경 출력(ProRes 4444), 외부 SRT/VTT/JSON 자막 타임라인, JSON 쇼트 시트 기반 멀티 쇼트/트랜지션, 음성 파일 길이에 맞춘 자동 영상 길이 조정, 단일 명령으로 여러 플랫폼 비율(틱톡/샤오홍슈/인스타그램/스퀘어/Bilibili/유튜브) 일괄 출력, `--resume`을 통한 중단 지점부터 재개, `--json`을 통한 AI 에이전트 친화적 구조화된 출력을 지원합니다. 템플릿은 `SF`(면적 비례 스케일 팩터)를 통해 다양한 해상도에서 일관된 타이포그래피를 유지하며, 자체 애니메이션을 위해 `animation-skeleton.html` 스켈레톤을 제공합니다.

지원 대상~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/wx4766/html-to-video/tree/main

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

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

문서

html-to-video · 코드로 프레임별 영상 제작

이 도구는 무엇인가요?

AI 영상 생성이 아닙니다. 코드로 프레임을 하나하나 그려 영상을 만듭니다:

HTML/Canvas를 작성하고, 화면을 frame(t) 순수 함수로 구성
   → Headless 브라우저가 t 시점으로 seek하여 스크린샷
   → ffmpeg가 H.264 MP4로 인코딩
자막 / 카메라 움직임 / 트랜지션을 모두 코드로 관리, 코드 수정 후 재렌더링

장점: 결정론적(동일 입력 → 동일 출력), 프레임 단위 제어 가능, 완벽한 한글 텍스트, 추가 비용 없음. 가장 적합한 용도: 설명 애니메이션, 동적 다이어그램, 제품 데모, 데이터 시각화 애니메이션, 홍보 영상 인트로. 부적합한 용도: 실사 느낌/분위기 샷 (이런 경우 VideoGen 사용).

이 도구로 무엇을 만들 수 있나요? (assets/templates/ 디렉토리의 11개 템플릿은 텍스트만 변경하면 바로 사용 가능):

템플릿용도출력 길이
explainer.html멀티 쇼트 설명 영상: 인트로 / 캐릭터+말풍선 / 핵심 포인트 / 막대 비교 / 아웃트로, 쇼트 간 트랜지션 포함, JSON 쇼트 시트 입력 가능; 가로 화면에서 정확히 3개 포인트일 때 자동으로 3열 배치, 세로 화면(1080×1920)은 상하 레이아웃 제공--duration=17.6
title-card.html동영상 타이틀 카드: 메인 타이틀이 한 글자씩 나타나고 서브타이틀 표시--duration=6
bar-chart.html막대 차트/순위: 막대가 순서대로 자라나고 숫자가 함께 증가--duration=6
line-chart.html꺾은선 트렌드: 선이 그려지고 데이터 포인트가 강조되며 마지막 포인트에 레이블 표시--duration=8
number-counter.html큰 숫자/KPI: 0에서 목표값까지 롤링되며 증감 레이블 표시--duration=7
timeline.html타임라인/프로세스: 중앙 축이 성장하고 각 노드가 순서대로 강조--duration=7
vertical-card.html세로 9:16 카드: 큰 글자 타이틀 + 큰 숫자, TikTok/WeChat Video/Reels 용--duration=6 --width=1080 --height=1920
compare.html전후 비교: 좌우 두 열로 비교, 하단에 결론 라인--duration=7
flow-steps.html플로우차트 애니메이션: 단계별로 순서대로 강조, 현재 단계 하이라이트--duration=7
subtitle-card.html문장별 설명 카드: 한 문장씩 표시되며 하단에 진행률 표시줄--duration=8 + --srt
vertical-subtitle.html세로 자막 카드: 키워드 강조, 9:16 네이티브 레이아웃--duration=8 --width=1080 --height=1920 + --srt

각 템플릿은 단일 파일이며 상단에 눈에 띄는 CFG 설정 블록이 있어 텍스트/데이터만 변경하면 됩니다 — canvas를 몰라도 영상을 만들 수 있습니다. 11개 템플릿 모두 테마 변경 지원 (아래 '색상 변경' 섹션 참조). 다른 것을 만들고 싶다면 assets/animation-skeleton.html을 참고하여 처음부터 작성하세요.

'한 조각의 애니메이션'이 아닌 '하나의 영상' 제작: assets/lib/scenes.js를 사용하여 멀티 쇼트와 트랜지션을 작성하세요. 쇼트 시트는 --story=shots.json으로 외부에서 주입 가능 — LLM이 수백 줄의 canvas 코드보다 JSON을 훨씬 안정적으로 작성합니다. 자세한 내용은 아래 '멀티 쇼트와 트랜지션' 섹션을 참조하세요.

크기: 템플릿이 뷰포트에 맞춰지므로 수동 수정 불필요

11개 템플릿 모두 window.innerWidth/innerHeight를 기준으로 캔버스를 설정하므로 어떤 크기로 변경해도 잘리지 않습니다:

# 가로 1080p (기본값)
bash assets/render.sh title-card.html --duration=6 --fps=60
# 세로 9:16
bash assets/render.sh title-card.html --duration=6 --fps=60 --width=1080 --height=1920
# 720p / 4K 도 동일
bash assets/render.sh title-card.html --duration=6 --fps=60 --width=1280 --height=720

템플릿 내부에서는 3단계 스케일 팩터를 사용하여 디자인을 실제 캔버스에 맞춥니다:

변수의미1920×10801080×1920
SX = W/1920가로 크기, 좌우 여백10.5625
SY = H/1080세로 크기, 줄 간격11.7778
SF = √(SX·SY)글자 크기, 선 두께, 모서리 둥글기, 블러11

글자 크기는 면적 비례(SF)를 사용하며 가로 폭을 따르지 않습니다 — 세로 화면으로 변경해도 글자가 작아지지 않으며 레이아웃만 펼쳐집니다. 1920×1080에서는 세 변수가 모두 1이므로 기본 출력이 이전 버전과 픽셀 단위로 일치합니다 (실측 PSNR = inf).

직접 작성한 애니메이션도 이 방식을 따라야 합니다. 그렇지 않으면 --width/--height를 변경할 때 오른쪽 아래가 조용히 잘릴 수 있습니다:

const cv = document.getElementById('stage');
cv.width  = window.innerWidth;      // ← 하드코딩 1920 금지
cv.height = window.innerHeight;     // ← 하드코딩 1080 금지

렌더러가 이제 캔버스 크기를 능동적으로 측정하여 뷰포트보다 작으면 명확하게 경고합니다 (더 이상 반쪽짜리 이미지를 조용히 출력하지 않음).

먼저 의존성 설치 (한 번만 수행)

렌더러는 Node 워크스페이스가 필요합니다 (playwright + 전체 버전 ffmpeg 포함). 한 줄 명령으로 설치:

bash scripts/install-deps.sh                       # 워크스페이스 기본 경로: ~/.workbuddy/binaries/node/workspace
bash scripts/install-deps.sh /path/to/workspace    # 또는 직접 경로 지정

자동으로 시스템의 브라우저 캐시를 감지하고, 일치하는 playwright 버전을 선택하며, 전체 버전 ffmpeg를 설치합니다. 워크스페이스 경로를 ~/.workbuddy/.html2video-env에 기록하며, 이후 render.sh / smoke.sh / qa.sh를 바로 사용할 수 있습니다.

수동 설치도 가능하지만 (워크스페이스에 playwright + @ffmpeg-installer/ffmpeg 포함), 직접 설정 파일을 작성해야 합니다:

# ~/.workbuddy/.html2video-env
WB_NODE_WORKSPACE="<node 워크스페이스 절대 경로>"

환경 변수를 사용하여 임시로 지정할 수도 있습니다: export WB_NODE_WORKSPACE="<경로>". 경로를 ${VAR:-기본값} 형태로 작성하지 마세요 — 사용자 이름에 작은따옴표가 포함된 경우(예: O'Brien), bash가 작은따옴표를 인용 부호 시작으로 인식하여 bad substitution 오류가 발생합니다.

네 가지 엄격한 제약 조건 (이미 겪었으니 반복하지 마세요)

  1. Playwright 버전이 브라우저 캐시와 일치해야 합니다. playwright-core@X는 자신의 browsers.json에 선언된 chromium 빌드 번호만 인식하며, 일치하지 않으면 "Executable doesn't exist" 오류를 발생시킵니다.

    → 버전을 추측하지 말고 scripts/install-deps.sh를 사용하세요. 이 스크립트가 scripts/detect-env.js를 실행하여 캐시를 감지한 후 자동으로 버전을 선택합니다. 별도 감지: node scripts/detect-env.js (출력: RECOMMEND_PW=...). 알려진 매핑: 빌드 1148→1.49.1, 1155→1.50.1, 1169→1.52.0, 1187→1.55.0, 1208→1.58.0.

    참고: 일부 네트워크 환경에서는 chromium 다운로드가 반복적으로 중단될 수 있습니다 (캐시 디렉토리에 반쪽짜리 불완전한 디렉토리가 남는 현상). 이 경우 업그레이드보다 기존 캐시를 재사용하는 것이 우선입니다 — 이것이 바로 감지 스크립트가 존재하는 이유입니다.

  2. Playwright에 내장된 ffmpeg는 사용할 수 없습니다. ms-playwright/ffmpeg-*/fffmpeg-win64.exe는 --disable-everything 플래그로 컴파일되어 VP8 코덱 + webm 컨테이너만 지원합니다. H.264, GIF, PNG 디코딩조차 지원하지 않습니다. 반드시 @ffmpeg-installer/ffmpeg의 전체 버전을 사용해야 합니다.

  3. ESM은 NODE_PATH를 인식하지 않습니다. .mjs 파일에서 import 'playwright'를 사용하면 반드시 ERR_MODULE_NOT_FOUND 오류가 발생합니다. → 모든 보조 스크립트는 **CJS (.cjs / require)**로 작성하세요.

  4. PATH는 신뢰할 수 없습니다. 특히 Git Bash / Windows에서는 PATH에 추가된 Windows 스타일 경로가 변환되면서 문제가 발생합니다. → PATH로 ffmpeg를 전달하지 말고, FFMPEG 환경 변수를 사용하세요 (렌더러가 지원함).

영상 제작流程

0단계 (가장 쉬운 방법): 템플릿을 선택하고 텍스트만 변경

cp assets/templates/bar-chart.html my-video.html
# my-video.html을 열고 상단 CFG의 title / data / note만 수정
bash assets/render.sh my-video.html --duration=6 --fps=60

템플릿은 자막 안전 영역을 고려하여 설계되었습니다: 텍스트가 화면 하단 15% 안에 들어가지 않습니다 (자막을 하드코딩하면 가려짐). 직접 레이아웃을 만들 때도 이 원칙을 따르세요.

1단계: 애니메이션 HTML 작성 (템플릿이 부족할 때)

assets/animation-skeleton.html을 스켈레톤으로 사용하세요. 핵심 계약 (렌더러가 이 계약에 의존):

// 다음 두 가지를 반드시 노출해야 함
window.__seek = (t) => { /* t 시점의 화면을 완전히 다시 그림 */ };
window.__ready = true;                 // 준비 신호

// 렌더러가 window.__seekRender = true를 주입하며,
// 이 경우 【자체 타이머를 반드시 고정】하고, 외부 __seek에 의해 완전히 프레임이 결정되도록 해야 함 (결정론 보장)
if (window.__seekRender) { renderAt(0); window.__ready = true; }
else { /* 브라우저 미리보기: requestAnimationFrame 자체 루프 사용 */ }

철칙: 각 프레임의 화면은 시간 t의 순수 함수여야 합니다. '이전 프레임'의 상태에 의존하지 말고, Math.random() / Date.now() / performance.now()를 사용하지 마세요 — 동일 입력에 다른 출력이 발생하고, QA의 '결정론'이 위반되며, 멀티 워커 병렬 렌더링 시 각각 다른 결과를 냅니다.

캔버스가 유일한 출력입니다: <canvas width=1920 height=1080>, CSS는 left:0; top:0, body는 margin:0. 렌더러는 clip:{x:0,y:0,width:W,height:H}로 왼쪽 위 모서리를 캡처하므로, 캔버스는 반드시 뷰포트를 정확히 채워야 하며 중앙에 배치되지 않아야 합니다.

(렌더러는 DOM/CSS/SVG 애니메이션도 지원하며, 동일하게 __seek만 노출하면 됩니다 — 하지만 DOM 애니메이션은 프레임당 리플로우가 느리므로, 순수 canvas가 일반적으로 더 안정적이고 빠릅니다.)

사용 금지 클래스 이름 (렌더러가 주입하는 CSS가 조건 없이 숨기므로 요소가 사라집니다): .no-record .progress .progress-bar .counter .tCur .phases .phase-label .phase .replay .masthead .kicker .title .footer 또한 position:fixed/sticky 요소에 <button>을 추가하지 마세요. 휴리스틱 규칙에 의해 재생 컨트롤로 간주되어 숨겨집니다. (특정 요소가 최종 영상에 나타나도록 하려면 data-record-keep="true" 속성을 추가하면 휴리스틱 숨김에서 제외됩니다.)

중국어 폰트: '"Microsoft YaHei", "PingFang SC", system-ui, sans-serif'; 고정폭은 'ui-monospace, Consolas, monospace'.

2단계: 스모크 테스트 (생략하지 마세요)

먼저 몇 프레임만 캡처하여 계약이 제대로 작동하는지, 콘솔 오류가 없는지, 화면이 깨지지 않는지 확인한 후 전체 렌더링을 진행하세요:

bash assets/smoke.sh animation.html
# SMOKE_T="0,1,2" SMOKE_W=1280 SMOKE_H=720 bash assets/smoke.sh animation.html

더 빠른 반복 방식: 짧은 구간만 렌더링하여 몇 초 만에 동적 효과를 확인할 수 있습니다.

bash assets/render.sh animation.html --from=3 --to=5 --fps=30   # 3초~5초만 렌더링
bash assets/render.sh animation.html --stills=1,3,5            # 세 개의 정지 프레임만 내보내기

3단계: 전체 렌더링

bash assets/render.sh animation.html --duration=12 --fps=60 --width=1920 --height=1080 --concurrency=4

주요 매개변수:

매개변수설명
--duration / --from / --to길이 / 특정 구간만 렌더링 (초). 범위가 지정된 경우 출력 파일명이 이름.시작-끝s.mp4로 변경되며, 완성된 파일을 덮어쓰지 않음
--stills=0.5,2.0해당 시점의 PNG만 내보내고 인코딩하지 않음
--fps / --width / --height프레임 레이트와 해상도
--preset=<이름>플랫폼별 사전 설정 크기: douyin / tiktok / vertical (1080×1920), xiaohongshu (1080×1440), instagram (1080×1350), square (1080×1080), bilibili / youtube / landscape (1920×1080)
--presets=a,b,c한 번의 명령으로 모든 비율 출력, 각 비율별로 하나의 완성 영상 생성 (파일명에 자동으로 -비율이름 접미사 추가). 크로스 플랫폼 배포 시 사용
--theme=<이름>테마 변경 (midnight / paper / neon), 템플릿의 CFG.theme를 재정의하며 파일 수정 불필요. 잘못된 이름 입력 시 명확한 경고와 함께 midnight으로 폴백
--out=<파일>출력 파일 경로 지정 (기본값: 동일 디렉토리, 동일 이름). 하나의 HTML에서 여러 색상을 출력할 때 사용하며, 그렇지 않으면 나중에 렌더링된 파일이 이전 파일을 덮어씀; 상위 디렉토리가 없으면 자동 생성
--jsonstdout에 결과 JSON 한 줄만 출력 (output / duration / frames / warnings 등), 사람이 읽는 로그는 모두 stderr로 전송. Agent가 파싱하기 위해 사용
--concurrency병렬 워커 수 (각 워커는 하나의 Chromium 페이지를 사용하므로 메모리 주의). auto로 설정 시 CPU와 메모리에 따라 자동 선택
--audio=<file>인코딩 후 자동으로 오디오 믹싱 (비디오 스트림은 직접 copy, 재인코딩 없음). --duration이 없으면 오디오 길이에 맞춰 자동으로 영상 길이 결정 (가장 가까운 전체 프레임으로 올림, 마지막 소리가 잘리지 않음)
--audio-fit=trim|pad오디오와 비디오 길이가 일치하지 않을 때: trim은 오디오에 맞춰 잘라냄 (기본값) / pad는 오디오에 무음을 추가하여 비디오 길이 유지
--gop=<초>키프레임 간격, 예: --gop=1은 초당 하나의 키프레임, 편집 시 초 단위 seek에 용이
--keep-frames성공 시에도 프레임별 PNG 유지 (CRF 재인코딩 시 다시 캡처할 필요 없음)
--tmpdir=<디렉토리>프레임별 PNG를 저장할 디스크 위치 (기본값: HTML과 같은 디렉토리). 1080p 1분 약 2.4 GB, 시스템 디스크 여유가 없을 때 다른 디스크 지정
--resume이전 렌더링이 중간에 중단됨? 이어서 렌더링, 이미 렌더링된 프레임은 재사용 (매개변수가 이전과 다르면 거부, 아래 참조)
--alpha투명 배경 출력 (ProRes 4444 .mov), 실사/AI 생성 영상 위에 오버레이하기 위해 사용, 아래 참조
--srt=<file>외부 자막 타임라인 (SRT / VTT / JSON), window.__cues 주입, 아래 참조. 동의어: --cues
--cue-log=<file>frame,t,cue_index,cue_start,cue_end CSV 내보내기, 자막이 정확히 동기화되었는지 확인하는 데 사용
--story=<file>쇼트 시트 JSON, window.__story 주입, 전체 대본 교체 (아래 '멀티 쇼트와 트랜지션' 참조)
--scene-log=<file>쇼트 타임라인 CSV 내보내기; 쇼트 시트 총 길이가 --duration과 일치하지 않으면 경고
--gif[=fps,너비]출력 후 GIF 생성 (기본값 15fps / 너비 960), 2단계 팔레트 최적화, 재렌더링 불필요
--poster=<초>출력 후 커버 정지 프레임 .jpg 생성, 웹사이트/소셜 미디어용
--batch=<file.json>작업 목록을 순서대로 렌더링하고 최종 요약 테이블 생성; 개별 작업 실패가 이미 완료된 작업에 영향을 주지 않으나 전체 종료 코드는 0이 아님
--headed브라우저 창을 열어 렌더링 (디버깅용; 동시성 자동으로 1로 감소, 창이 여러 개 뜨는 것 방지)
--allow-errors페이지 오류를 실패로 간주하지 않음 (기본값: 오류 발생 시 → 0이 아닌 종료 코드)
--keep-chrome재생 컨트롤을 숨기지 않음 (디버깅용)
--readytimeout=8__seek 준비를 기다리는 시간(초) (시작이 느린 큰 폰트/대용량 데이터의 경우 증가)
--no-servefile://로 페이지 로드 (기본값은 로컬 http 서버 사용, 아래 참조)

ESM / fetch 프로젝트를 바로 렌더링 가능. 렌더러는 기본적으로 127.0.0.1에 바인딩된 로컬 http 서버를 시작하여 페이지를 로드합니다 — file://에서는 <script type="module">과 fetch()가 CORS에 의해 차단되므로 Vite/esbuild로 구성된 애니메이션이나 로컬 JSON 파일을 읽는 프로젝트는 실행되지 않습니다. http를 사용하면 이러한 문제가 없습니다. 렌더링 시작 시 이 주소가 출력되므로, 문제 해결 시 이 주소를 브라우저에 직접 붙여넣어 콘솔을 확인하는 것이 가장 편리합니다. 서버 시작에 실패하면 자동으로 file://로 폴백하고 경고합니다 (경고만, 실패로 간주하지 않음).

  • 출력 파일: HTML과 동일한 이름의 .mp4
  • 프레임 수 = fps × duration. 12초@60fps = 720 프레임, 실제 소요 시간 약 6~7분 (concurrency=4)
  • 인코딩 약 1분 (crf 18 / preset medium)

렌더링 종료 시 페이지 오류가 있으면 종료 코드가 0이 아닙니다 (하지만 완성된 영상은 일반적으로 생성되므로 확인 가능).

렌더링이 중간에 중단됨, 처음부터 다시 시작하지 마세요

긴 영상(60fps × 30s = 1800 프레임)이 80% 진행 중에 중단되면 처음부터 다시 시작하는 것이 가장 손해입니다. --resume을 추가하면 이어서 렌더링합니다:

bash assets/render.sh animation.html --fps=60 --duration=30 --resume
# ▸ resume: 1432 프레임 재사용, 368 프레임 추가 렌더링 필요

세 가지 규칙은 모두 '이어서 만든 영상이 한 번에 완성한 영상과 완전히 동일하도록' 하기 위한 것입니다:

  • 프레임은 먼저 .part로 작성된 후 이름이 변경됩니다. kill된 프레임은 반쪽짜리 PNG를 남겨 '완료'로 위장하지 않으므로, 재사용되는 프레임은 모두 실제로 완전히 그려진 것입니다.
  • 매개변수가 변경되면 바로 거부합니다. 이전에는 30fps였는데 지금은 60fps이면 두 배치의 프레임이 섞여 인코딩되어 영상 앞뒤 리듬이 일관되지 않고 오류도 발생하지 않습니다 — 따라서 fps / 크기 / 범위 / 렌더링 모드 중 하나라도 다르면 '이전 vs 현재'를 출력하고 종료하여 사용자가 직접 삭제 여부를 결정하도록 합니다.
  • 프레임 번호는 연속적이어야 합니다. 한 장이 누락되면 ffmpeg가 조용히 일찍 종료되어 영상이 짧아지므로, 인코딩 전에 [시작, 끝) 범위의 프레임 번호를 하나씩 확인하고, 누락이 있으면 보고하고 --resume을 추가하라는 메시지를 표시합니다.

임시 디렉토리는 HTML 옆의 .seek-tmp-resume/에 고정됩니다 (--resume 없이도 일회성 디렉토리를 사용하므로 병렬 렌더링이 서로 간섭하지 않음). 렌더링 완료 후 자동으로 삭제됨; 중간에 유지하려면 --keep-frames를 지정하세요. 이것은 의도적인 설계입니다: 페이지 오류는 종종 '전체 화면 검은색 / 화면 절반 콘텐츠 누락'을 의미하며, 이전 버전에서는 오류가 발생해도 성공으로 표시되다가 나중에 프레임을 다시 확인할 때야 문제를 발견했습니다 — 이제는 인코딩 전에 오류를 출력하고 최종적으로 실패로 처리합니다.

투명 출력: 텍스트 레이어를 다른 사람이 촬영한 영상 위에 오버레이

AI 생성 배경 + 코드 생성 텍스트 레이어는 현재 가장 비용 효율적인 조합입니다 (AI가 텍스트를 그리면 반드시 흐릿해지고, 코드는 픽셀 단위로 정확하게 그립니다). --alpha가 이 경로를 위해 준비되었습니다:

bash assets/render.sh animation.html --duration=6 --fps=30 --alpha
# → animation.mov (ProRes 4444 / yuva444p10le, 배경 투명)

템플릿 측에서 한 가지를配合해야 합니다: window.__transparent가 참일 때 배경 레이어 그리기를 건너뜁니다. 현재 title-card, compare, flow-steps, subtitle-card, vertical-subtitle, explainer의 6개 템플릿이 이미 이렇게 작성되어 있으므로 --alpha를 추가하면 바로 투명 효과를 얻을 수 있습니다. 나머지 5개는 전체 화면 차트(막대 / 꺾은선 / 큰 숫자 / 타임라인 / 세로 카드)로 배경색이 필요하므로 --alpha를 추가해도 불투명한 영상이 생성됩니다. 직접 작성한 애니메이션은 다음 방식을 따르세요:

const TRANSPARENT = !!window.__transparent;
function drawFrame(t) {
  g.clearRect(0, 0, W, H);
  if (!TRANSPARENT) {
    // 배경색 / 전체 화면 그라데이션 / 광원 효과 — 모두 건너뛰어야 함
    g.fillStyle = '#080A10';
    g.fillRect(0, 0, W, H);
  }
  // 텍스트, 도형은 평소대로 그림
}

전체 화면 반투명 광원 효과도 함께 건너뛰어야 합니다. 불투명도가 5%에 불과하더라도 실사 영상 위에 오버레이되면 전체 영역이 희게 보여 편집자의 첫 반응은 '이 소스 더럽다'입니다. 실측 결과: 배경색만 건너뛰고 광원 효과는 남겼을 때 네 모서리 alpha=12 (0이 아님); 모두 건너뛰었을 때 네 모서리 alpha=0, 불투명한 픽셀은 3.7%에 불과 (모두 텍스트).

크기 비용 (960×540 / 2초 / 20프레임 실측):

출력크기용도
H.264 .mp4118 KB완성 영상 직접 배포
ProRes 4444 .mov760 KB (6.5배)편집 소프트웨어로 가져와 레이어 오버레이

ProRes만 지원, VP9는 지원 안 함. --pix_fmt yuva420p + -auto-alt-ref 0을 시도했을 때, 파일 크기는 7.8KB에서 16.8KB로 증가했지만 (알파 데이터가 기록됨) Chromium 재생 시 전체 프레임의 alpha가 모두 0이었습니다 — 텍스트까지 투명해졌습니다. lossless, 고정 비트레이트, -lag-in-frames 0도 동일했습니다. 올바르게 동작함을 스스로 증명할 수 없으므로 옵션으로 제공하지 않습니다. 작은 크기의 투명 소스가 필요하면 ProRes를 편집 소프트웨어로 가져온 후 webm으로 내보내는 것은 편집 소프트웨어의 역할입니다.

오디오 싱크: 오디오 먼저, 그 다음 영상 길이 결정

실제 작업 과정은 '대본 작성 → 오디오 녹음 → 영상 제작'이지, 먼저 6초를 정하고 오디오를 억지로 맞추는 것이 아닙니다. 따라서 --audio가 주어지고 --duration이 없으면 비디오 길이가 오디오 길이에 맞춰 결정됩니다:

bash assets/render.sh title-card.html --audio=voice.m4a --story=shots.json
# ▸ audio: 오디오 12.48초, --duration 미지정 → 비디오 길이를 오디오에 맞춤 (12.5초, 가장 가까운 전체 프레임으로 올림)
  • 가장 가까운 전체 프레임으로 올림하며 반올림하지 않습니다: 반 프레임이 모자라면 마지막 글자의 끝소리가 잘립니다.
  • --duration이 명시적으로 주어졌고 오디오와의 차이가 0.35초를 초과하면 명확한 경고를 출력하며, 차이와 어느 쪽이 손상되는지 설명합니다. 이전 버전에서는 이 경우 조용히 짧은 영상이 생성되었습니다 — 화면 후반부 전체가 사라졌지만 로그에는 ✓ Done 한 줄만 있었습니다.
  • --audio-fit=pad: 오디오가 비디오보다 짧을 때 무음을 추가하여 전체 영상 길이를 유지 (기본값 trim은 오디오에 맞춰 잘라냄).

참고: trim 모드에서 완성된 영상은 정확히 오디오 길이와 일치하지 않습니다. 비디오 스트림은 -c:v copy로 직접 복사되므로, ffmpeg는 프레임 패킷 경계에서만 종료할 수 있으며, 실측 결과 0.3~0.5초가 더 남습니다. 밀리초 단위 정렬이 필요하면 재인코딩해야 하며, 이는 화질을 희생하여 정밀도를 얻는 것으로 가치가 없습니다 — 영상 끝에 몇 프레임이 더 남는 것은 일반적으로 눈에 띄지 않습니다.

한 번에 여러 플랫폼 배포: 한 명령으로 모든 플랫폼 비율 출력

동일한 영상을 TikTok, Xiaohongshu, Bilibili에 게시하려면 편집 소프트웨어에서 비율별로 시퀀스를 만들고 각각 내보내기를 모니터링해야 합니다. 여기서 레이아웃은 원래 파라미터화되어 있으므로, 여러 번 렌더링하는 것은 단순히 루프를 한 번 더 도는 것뿐입니다:

bash assets/render.sh bar-chart.html --duration=8 \
  --presets=douyin,xiaohongshu,square --out=dist/chart.mp4
# → dist/chart-douyin.mp4       1080×1920
#   dist/chart-xiaohongshu.mp4  1080×1440
#   dist/chart-square.mp4       1080×1080
프리셋크기대상 플랫폼
douyin / tiktok / vertical1080×1920Douyin, Kuaishou, WeChat Video, Reels, Shorts
xiaohongshu / xhs1080×1440Xiaohongshu 3:4
instagram / ig1080×1350IG 피드 4:5
square1080×1080정사각형 1:1
bilibili / youtube / landscape1920×1080가로 16:9
  • 단일 비율: --preset=xiaohongshu; 여러 비율: --presets=a,b (최소 2개, 하나만 필요하면 --preset 사용).
  • 프리셋 이름 오타 시 즉시 실패하고 사용 가능한 이름 목록을 출력 — 잘못된 비율로 1920×1080 영상을 조용히 렌더링하지 않습니다.
  • 레이아웃이 모든 비율을 지원하는지는 별개의 문제입니다: 템플릿은 뷰포트에自适应하지만, 9:16과 16:9의 정보 밀도는 다르며, 3:4에서는 글자가 너무 크거나 콘텐츠가 너무 좁을 수 있으므로 직접 확인해야 합니다. --presets는 크기만 보장하고 구도는 보장하지 않습니다.
  • 세로 화면 하단의 여백은 의도적인 것입니다. 레이아웃 오류가 아닙니다: 플랫폼 진행률 표시줄과 버튼이 그 위치에 있으므로, 템플릿은 하드코딩된 자막을 위해 15% 안전 영역을 통일적으로 남겨둡니다.
  • --stills와는 상호 배타적 (--presets는 여러 영상 출력, --stills는 스크린샷만); --batch와 중첩 불가.

AI Agent용: --json

이 도구의 주요 호출자는 AI Agent입니다. 사람은 ✓ Done: xxx.mp4 (1.2 MB · 30fps · 12s)를 읽을 수 있지만, Agent는 정규식으로 추측해야 합니다 — 경로에 공백, 한글, 괄호가 있으면 실패합니다. --json은 결과를 구조화합니다:

bash assets/render.sh title-card.html --duration=6 --json
{"ok":true,"exitCode":0,"input":"…/title-card.html","output":"…/title-card.mp4",
 "width":1920,"height":1080,"fps":60,"duration":6,"frames":360,"bytes":2411724,
 "gif":null,"poster":null,"warnings":[],"mode":"video","theme":null,"preset":null}
  • stdout에는 이 한 줄만 출력되며, 진행률과 사람이 읽는 로그는 모두 stderr로 리디렉션됩니다 — JSON.parse(stdout)으로 바로 사용 가능.
  • warnings는 배열입니다: 매개변수 충돌, 오디오/비디오 길이 불일치, 알 수 없는 테마 이름 등이 여기에 포함됩니다. 빈 배열 = 깨끗함.
  • duration은 완성된 영상의 실제 길이 (ffmpeg가 읽어온 값)이며, 계획된 값이 아닙니다.
  • --presets 사용 시 output은 배열이 되며, presets: [{name,width,height,output,ok,bytes}]가 추가됩니다.

외부 타임라인으로 자막 관리: 오디오가 변경되면 자막도 따라감

자막 텍스트를 템플릿의 CFG.lines에 작성하는 것이 가장 간단하지만, 오디오가 필요한 경우 타임라인을 오디오에 맞춰야 합니다 — 매 문장마다 수동으로 초를 세는 것은 비효율적입니다. 따라서 자막 파일을 직접 제공하는 것을 지원합니다:

bash assets/render.sh subtitle-card.html --duration=8 --srt=voice.srt --cue-log=cues.csv
  • SRT (HH:MM:SS,mmm), WebVTT (MM:SS.mmm도 가능), JSON ([{start,end,text}])을 지원합니다.
  • 한 번 파싱되어 window.__cues로 주입되며, 템플릿은 현재 프레임의 t에 따라 해당하는 자막을 스스로 찾습니다. --srt 사용 시 템플릿은 __cues를 우선 읽고, 없으면 CFG.lines로 폴백하므로 동일한 템플릿이 두 가지 방식 모두에서 작동합니다.
  • --cue-log는 frame,t,cue_index,cue_start,cue_end CSV를 출력합니다. '자막 시간이 맞는지'를 계산 가능한 숫자로 만듭니다: 각 문장의 첫 프레임 시간에서 SRT 시작점을 빼면 오차입니다. 실측 결과 4문장 / 80프레임에서 각 문장의 오차는 0 ms, 프레임 커버리지는 이론값과 완전히 일치합니다.

템플릿 측에서는 두 줄만 추가하면 지원할 수 있습니다:

const cues = (window.__cues && window.__cues.length) ? window.__cues : CFG.lines;
const cur = cues.find(c => t >= c.start && t < c.end) || cues[cues.length - 1];

자막이 하단 15% 안으로 들어가지 않도록 하세요. 이 영역은 각 플랫폼의 진행률 표시줄 / 좋아요 버튼 영역이므로, 거기에 작성해도 보이지 않습니다. qa.js는 '자막이 하단 영역을 침범함'을 경고하며, 네 개의 새 템플릿의 세로 레이아웃은 이 안전 영역을 확보합니다.

색상 변경: 하나의 필드만 수정, 캔버스 코드 수정 불필요

assets/templates/themes.js에는 세 가지 색상 팔레트가 있으며, 11개 템플릿 모두 연동되어 있습니다. 두 가지 변경 방법:

# ① 명령줄에서 변경 (권장): 어떤 파일도 수정하지 않고, 한 번 렌더링할 때 하나의 테마 적용
bash assets/render.sh bar-chart.html --duration=6 --theme=paper --out=bar-paper.mp4
# ② 파일 수정: 템플릿 상단 CFG에 하드코딩
const CFG = {
  theme: 'paper',   // midnight (기본, 어두움) | paper (밝음) | neon (검은 보라 네온)
  ...
};

명령줄이 CFG.theme보다 우선합니다 (렌더러가 window.__h2vTheme를 주입하며, 템플릿은 h2vTheme(window.__h2vTheme || CFG.theme)를 읽습니다). 따라서 동일한 HTML로 세 가지 색상을 일괄 출력할 수 있습니다:

관련 스킬