Sangcho.log
dev

HyperFrames 탐구 1 — URL 하나가 MP4가 되기까지, 영화 제작소의 7단계

11 min read|

OpenCut 시리즈에 이어 두 번째 오픈소스 탐구입니다. 이번 대상은 HyperFrames — 유저가 /website-to-hyperframes <url>이라고 치면 그 웹사이트의 홍보 영상 MP4가 나오는 도구입니다. OpenCut이 "사람이 손으로 편집하는 브라우저 비디오 에디터"라면, HyperFrames는 **"AI 에이전트가 영상을 만드는 파이프라인"**입니다.

이 시스템은 영화 제작소에 빗대면 정확히 이해됩니다. 로케이션 헌팅을 하고, 대본과 콘티를 쓰고, 성우를 녹음하고, 촬영하고, 시사회를 거쳐 최종본을 뽑는 — 그 순서 그대로입니다. 이번 편에서는 URL 입력부터 MP4가 나오기까지를 단계별로 추적합니다.

시작: 스킬이 파이프라인을 가르친다

유저가 URL을 주는 순간, Claude Code의 스킬 시스템이 발동합니다.

.claude/skills/website-to-hyperframes/
├── SKILL.md                   # 스킬의 entry point (트리거 + 7단계 메소드)
└── references/
    ├── step-1-capture.md      # 각 단계 상세 가이드
    ├── step-2-design.md
    └── ...

SKILL.md의 frontmatter가 트리거("capture", "URL", "video" 등)를 정의하고, 발동되면 본문의 7단계 메소드가 컨텍스트에 주입됩니다. 각 단계의 상세 가이드는 해당 단계에 도달했을 때 Read 도구로 lazy-load합니다. 컨텍스트 부담 없이 깊은 가이드를 제공하는 패턴이죠. 에이전트 연동의 속사정은 2편에서 자세히 다룹니다.

1단계: 캡처 — 로케이션 헌팅

npx hyperframes capture <URL>이 실행되면 Puppeteer가 헤드리스 Chrome(1920×1080)을 띄워 사이트를 측정합니다.

  1. URL 방문 → networkidle 대기
  2. JS 인젝션으로 데이터 추출 — document.fonts에서 폰트 목록, getComputedStyle()로 상위 컬러(tokens.json), 헤딩·문단 텍스트(visible-text.txt), 이미지·SVG·비디오 메타데이터(assets-catalog.json)
  3. 스크롤하며 뷰포트 단위 스크린샷 ~20장 (30% 오버랩)
  4. 이미지·폰트 에셋 다운로드
  5. (옵션) Gemini Vision API로 각 에셋 한 줄 설명 생성
  6. AGENTS.md + CLAUDE.md 자동 생성 — 다음 단계의 에이전트가 읽을 요약 인덱스
capture/
├── screenshots/    (스크롤 PNG)
├── assets/         (이미지·SVG·폰트)
├── extracted/      (tokens.json, visible-text.txt, ...)
├── AGENTS.md       (에이전트용 요약)
└── CLAUDE.md       (Claude용 요약)

마지막 항목이 이 프로젝트다운 부분입니다. 캡처 결과물에 **"이걸 읽을 에이전트를 위한 안내문"**까지 함께 생성합니다.

2~4단계: 문서 — 의사결정을 압축한다

여기서는 CLI가 돌지 않습니다. 순수하게 Claude가 캡처 데이터를 읽고 세 개의 마크다운을 씁니다. 영화로 치면 제작 바이블, 대본, 콘티입니다.

파일 역할 입력 출력
DESIGN.md 브랜드 치트시트 tokens.json + 스크린샷 6섹션 (~90줄)
SCRIPT.md 내레이션 대본 visible-text.txt + DESIGN.md 비트(Beat)별 한 줄씩
STORYBOARD.md 비트별 연출 노트 DESIGN.md + SCRIPT.md + 에셋 비트별 visual/animation/transition/SFX

이 세 문서가 이후 모든 단계의 single source of truth가 됩니다. 즉흥 연출은 없습니다 — 색과 폰트는 DESIGN.md에서, 타이밍은 SCRIPT.md에서, 장면 구성은 STORYBOARD.md에서 나옵니다.

5단계: 음성 — 타이밍을 측정한다

hyperframes tts narration.txt -v af_nova -o narration.wav
hyperframes transcribe narration.wav

TTS는 Node.js가 Python 프로세스를 스폰해 kokoro-onnx(~80MB 모델, ONNX Runtime CPU 추론)로 24kHz WAV를 만들고, transcribe는 FFmpeg로 16kHz mono 변환 후 whisper.cpp(small.en)가 단어별 [start, end] 타임스탬프를 뽑아 transcript.json으로 저장합니다.

이 단계의 진짜 의미는 **"비트별 길이가 명확한 숫자로 확정된다"**는 것입니다. "Beat 1은 02.7초, Beat 2는 2.76.3초" — 이후 모든 애니메이션이 이 숫자에 맞춰 코딩됩니다. 음성을 먼저 만들고 영상을 거기에 맞추는, 순서가 뒤집힌 제작 방식이죠.

6단계: 컴포지션 — HTML이 곧 비디오 소스코드

Claude가 index.htmlcompositions/beat-*.html을 작성합니다.

<!-- 루트 index.html -->
<audio id="narration" data-start="0" data-duration="22"></audio>
<div id="beat-1" data-composition-src="compositions/beat-1.html"
     data-start="0"   data-duration="2.69"></div>
<div id="beat-2" data-composition-src="compositions/beat-2.html"
     data-start="2.7" data-duration="3.59"></div>
<script>
  window.__timelines["root"] = gsap.timeline({ paused: true });
</script>

data-start, data-duration 속성이 타임라인이고, 각 비트 HTML은 자신의 <style> + <script> + GSAP 타임라인을 <template> 안에 등록합니다. 별도의 프로젝트 파일 포맷이 없습니다. HTML 자체가 영상의 데이터 모델입니다.

7단계: 검증 — 시사회는 싼 것부터

렌더링은 비쌉니다(5~10분). 그래서 검증은 가장 가벼운 것부터 단계적으로 돕니다.

도구 소요 하는 일
lint 1초 정적 분석 — GSAP·CSS transform 충돌, 무한 repeat 등 100여 개 규칙
validate 30초 헤드리스 Chrome에서 5개 시점 seek — 콘솔 에러, 404/CORS, 텍스트 배경 픽셀 샘플링으로 WCAG 대비 계산
snapshot 1분 각 비트 중간 지점을 PNG로 캡처 — 육안 검사용
preview 즉시 Vite 서버 + <hyperframes-player> 웹 컴포넌트로 브라우저에서 재생/스크럽
render 5~10분 최종 승인 후에만 실행

8단계: 렌더링 — 결정적 캡처 루프

hyperframes render의 심장부는 이 루프입니다.

for (let frame = 0; frame < fps * duration; frame++) {
  const t = frame / fps;                        // 30fps × 22s = 660 프레임
  masterTl.seek(t);                             // GSAP 타임라인을 t로 스크럽
  await renderAllPendingPaints();               // 페인트 완료 대기
  await videoElements.forEach(seekToMediaTime); // <video>도 currentTime 강제 설정
  const png = await page.screenshot({ type: 'png' });
}

시간이 흐르는 것을 기다리는 게 아니라, 타임라인을 프레임 단위로 강제로 옮기며 한 장씩 찍습니다. 그다음 FFmpeg가 오디오 트랙들(data-start/data-volume 적용해 믹스)과 프레임들을 합쳐 MP4로 인코딩합니다.

이게 성립하려면 결정성(determinism) — 같은 HTML + 같은 시각 t → 반드시 같은 픽셀 — 이 필요합니다. 그래서 Date.now()Math.random()은 금지, GSAP 타임라인은 paused: true 필수, 비디오는 직접 .play() 금지(프레임워크가 매 프레임 currentTime을 설정)라는 규칙이 강제됩니다. 이 결정성 이야기는 3편(테스트)과 4편(렌더 엔진)의 주인공이 됩니다.

전체 구조와 제작 철학

USER: /website-to-hyperframes <url>
   ↓
[준비 — 전체 시간의 60%]
   ① CAPTURE    "사이트를 측정한다"     (Puppeteer)
   ② DOCUMENTS  "의사결정을 압축한다"   (DESIGN/SCRIPT/STORYBOARD.md)
   ③ VOICE      "타이밍을 측정한다"     (Kokoro TTS + whisper.cpp)
   ↓
[실행 — 30%]
   ④ COMPOSITION "실제로 만든다"        (HTML + GSAP)
   ↓
[검증 — 10%]
   ⑤ VALIDATE & DELIVER                (lint → validate → snapshot → preview → render)
   ↓
🎬 renders/output.mp4

패키지 구조도 이 흐름을 그대로 반영합니다 — capture(측정), core(런타임·타임라인 합성), engine(헤드리스 Chrome 캡처), producer(렌더 파이프라인), player/studio(미리보기), shader-transitions(WebGL 트랜지션).

철학은 한 문장입니다. "준비를 길게 한 만큼, 실행이 짧고 정확해진다." 전체 시간의 60%를 측정과 문서화에 쓰는 이유는, 가장 비싼 작업(렌더링과 재작업)을 가장 싸게 만들기 위해서입니다.

더 깊이: "브라우저를 렌더 팜으로" 계보 — Remotion과의 비교

"헤드리스 브라우저로 프레임을 찍어 FFmpeg로 인코딩한다"는 아이디어 자체는 HyperFrames의 발명이 아닙니다. Remotion이 같은 구조로 유명하죠 — React 컴포넌트가 useCurrentFrame()으로 현재 프레임을 받아 화면을 그리면, 렌더러가 프레임을 순회하며 캡처합니다.

차이는 누가 쓰라고 만든 도구인가에 있습니다.

  • Remotion — 개발자가 React로 영상을 프로그래밍하는 도구. 컴포넌트, props, TypeScript가 인터페이스
  • HyperFrames — AI 에이전트가 순수 HTML + data-* 속성으로 영상을 작성하는 도구. 마크다운 문서와 검증 CLI가 인터페이스

React 추상화 없이 순수 HTML을 고집한 것, 그리고 파이프라인의 60%를 "에이전트가 읽을 문서 만들기"에 배정한 것 — 이 선택들은 전부 "저자가 사람이 아니라 LLM"이라는 전제에서 나옵니다. 같은 렌더링 기술 위에서 인터페이스 설계가 사용자에 따라 어떻게 갈라지는지 보여주는 좋은 대조입니다.


한 줄 요약 — HyperFrames는 "측정(60%) → 코딩(30%) → 검증(10%)"의 영화 제작 파이프라인으로, HTML을 비디오 소스코드 삼아 헤드리스 Chrome이 결정적으로 매 프레임을 렌더하고 FFmpeg가 MP4로 인코딩하는 시스템이다.

스스로 점검할 질문

  1. 음성(TTS)을 영상보다 먼저 만드는 순서가 왜 합리적인지 설명할 수 있나요?
  2. 검증 단계가 lint(1초) → validate(30초) → snapshot(1분) → render(5~10분) 순인 이유는 무엇인가요?
  3. Date.now()Math.random()이 금지되는 이유를 "결정성" 관점에서 설명할 수 있나요?

참고 링크