• Hyperframes๋Š” HTML์„ ํ”„๋ ˆ์ž„ ๋‹จ์œ„๋กœ ์บก์ฒ˜ํ•˜์—ฌ ๊ฒฐ์ •๋ก ์  MP4 ๋น„๋””์˜ค๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ์˜คํ”ˆ์†Œ์Šค ํ”„๋ ˆ์ž„์›Œํฌ (Apache 2.0)
  • ์›น ํŽ˜์ด์ง€๋ฅผ ๋งŒ๋“œ๋Š” ๋ฐฉ์‹์œผ๋กœ ๋น„๋””์˜ค๋ฅผ ์ •์˜ํ•˜๋Š” HTML-native ๋น„๋””์˜ค ์ €์ž‘ ๋„๊ตฌ
  • AI ์ฝ”๋”ฉ ์—์ด์ „ํŠธ๊ฐ€ HTML์„ ์ง์ ‘ ์ž‘์„ฑํ•˜์—ฌ ๋น„๋””์˜ค๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋„๋ก ์„ค๊ณ„๋œ ์—์ด์ „ํŠธ ์นœํ™”์  ๋ Œ๋”๋ง ์—”์ง„

ํ•ด๋‹น ๊ฐœ๋…์ด ํ•„์š”ํ•œ ์ด์œ 

  • ๊ธฐ์กด ๋น„๋””์˜ค ํŽธ์ง‘ ๋„๊ตฌ(Premiere, After Effects)๋Š” GUI ๊ธฐ๋ฐ˜์ด๋ผ ์ž๋™ํ™”ยท์Šคํฌ๋ฆฝํŒ…์ด ์–ด๋ ต๋‹ค
  • Remotion์€ React ๊ธฐ๋ฐ˜์ด๋ผ ๋นŒ๋“œ ์Šคํ…์ด ํ•„์š”ํ•˜๊ณ , ์—์ด์ „ํŠธ๊ฐ€ JSX๋ฅผ ์ •ํ™•ํžˆ ์ƒ์„ฑํ•˜๊ธฐ ์–ด๋ ต๋‹ค
  • Hyperframes๋Š” plain HTML๋งŒ์œผ๋กœ ๋น„๋””์˜ค๋ฅผ ์ •์˜ํ•˜๋ฏ€๋กœ, ์ฝ”๋”ฉ ์—์ด์ „ํŠธ๋“  ์‚ฌ๋žŒ์ด๋“  ์ง„์ž… ์žฅ๋ฒฝ์ด ๋‚ฎ๋‹ค
  • ๋™์ผ ์ž…๋ ฅ โ†’ ๋™์ผ ์ถœ๋ ฅ(๊ฒฐ์ •๋ก ์  ๋ Œ๋”๋ง)์ด ๋ณด์žฅ๋˜์–ด CI/CD, ํšŒ๊ท€ ํ…Œ์ŠคํŠธ, ์ž๋™ํ™” ํŒŒ์ดํ”„๋ผ์ธ์— ์ ํ•ฉํ•˜๋‹ค

AS-IS

sequenceDiagram
    autonumber
    participant ์‚ฌ์šฉ์ž
    participant GUIํŽธ์ง‘๊ธฐ as GUI ๋น„๋””์˜ค ํŽธ์ง‘๊ธฐ
    participant ์ถœ๋ ฅ as MP4 ์ถœ๋ ฅ

    ์‚ฌ์šฉ์ž->>GUIํŽธ์ง‘๊ธฐ: ์ˆ˜๋™์œผ๋กœ ํƒ€์ž„๋ผ์ธ ์กฐ์ž‘
    GUIํŽธ์ง‘๊ธฐ->>GUIํŽธ์ง‘๊ธฐ: ํ”„๋กœ์ ํŠธ ํŒŒ์ผ ์ €์žฅ (.prproj ๋“ฑ)
    ์‚ฌ์šฉ์ž->>GUIํŽธ์ง‘๊ธฐ: ๋‚ด๋ณด๋‚ด๊ธฐ ๋ฒ„ํŠผ ํด๋ฆญ
    GUIํŽธ์ง‘๊ธฐ->>์ถœ๋ ฅ: ๋ Œ๋”๋ง (์‹คํ–‰ ํ™˜๊ฒฝ์— ๋”ฐ๋ผ ๊ฒฐ๊ณผ ๋‹ค๋ฅผ ์ˆ˜ ์žˆ์Œ)
    Note over GUIํŽธ์ง‘๊ธฐ,์ถœ๋ ฅ: ์ž๋™ํ™” ๋ถˆ๊ฐ€, ์—์ด์ „ํŠธ ์ ‘๊ทผ ๋ถˆ๊ฐ€

TO-BE

sequenceDiagram
    autonumber
    participant Agent as AI ์—์ด์ „ํŠธ / ์‚ฌ์šฉ์ž
    participant HTML as index.html (Composition)
    participant CLI as Hyperframes CLI
    participant Chrome as Headless Chrome
    participant FFmpeg
    participant ์ถœ๋ ฅ as MP4 ์ถœ๋ ฅ

    Agent->>HTML: HTML + data attributes ์ž‘์„ฑ
    Agent->>CLI: npx hyperframes render
    CLI->>Chrome: ํ”„๋ ˆ์ž„๋ณ„ seekFrame(N) ํ˜ธ์ถœ
    Chrome->>Chrome: beginFrame API๋กœ ๊ฒฐ์ •๋ก ์  ์บก์ฒ˜
    Chrome->>FFmpeg: PNG ํ”„๋ ˆ์ž„ ์ŠคํŠธ๋ฆผ ์ „๋‹ฌ
    FFmpeg->>์ถœ๋ ฅ: x264 ์ธ์ฝ”๋”ฉ โ†’ MP4
    Note over Chrome,์ถœ๋ ฅ: ๋™์ผ ์ž…๋ ฅ = ๋™์ผ ์ถœ๋ ฅ (๊ฒฐ์ •๋ก ์ )

์•„ํ‚คํ…์ฒ˜ โ€” 6๊ฐœ ํŒจํ‚ค์ง€ ๊ตฌ์กฐ

Hyperframes๋Š” ๋ชจ๋…ธ๋ ˆํฌ ๊ตฌ์กฐ๋กœ, ๊ฐ ํŒจํ‚ค์ง€๊ฐ€ ๋ช…ํ™•ํ•œ ์—ญํ• ์„ ๋‹ด๋‹นํ•œ๋‹ค:

ํŒจํ‚ค์ง€์—ญํ• 
@hyperframes/coreํƒ€์ž… ์‹œ์Šคํ…œ, HTML ํŒŒ์„œ/์ œ๋„ˆ๋ ˆ์ดํ„ฐ, ๋ฆฐํ„ฐ, ๋Ÿฐํƒ€์ž„, Frame Adapter
@hyperframes/enginePuppeteer ๊ธฐ๋ฐ˜ Headless Chrome ํ”„๋ ˆ์ž„ ์บก์ฒ˜ ์—”์ง„
@hyperframes/producer์บก์ฒ˜ + FFmpeg ์ธ์ฝ”๋”ฉ + ์˜ค๋””์˜ค ๋ฏน์‹ฑ ํ†ตํ•ฉ ํŒŒ์ดํ”„๋ผ์ธ
@hyperframes/studio๋ธŒ๋ผ์šฐ์ € ๊ธฐ๋ฐ˜ ๋น„์ฃผ์–ผ ์—๋””ํ„ฐ UI
@hyperframes/player์ž„๋ฒ ๋“œ ๊ฐ€๋Šฅํ•œ <hyperframes-player> ์›น ์ปดํฌ๋„ŒํŠธ
hyperframes (CLI)init, preview, lint, render ๋“ฑ CLI ๋ช…๋ น์–ด

ํŒจํ‚ค์ง€ ์˜์กด ๊ด€๊ณ„:

CLI โ†’ Producer โ†’ Engine โ†’ Core
Studio โ†’ Core
Player โ†’ Core

Composition โ€” ๋น„๋””์˜ค์˜ ๋‹จ์œ„

Composition์€ Hyperframes ๋น„๋””์˜ค์˜ ๊ธฐ๋ณธ ๋‹จ์œ„๋กœ, ํ•˜๋‚˜์˜ HTML ํŒŒ์ผ์ด ํ•˜๋‚˜์˜ ๋น„๋””์˜ค(๋˜๋Š” ๋น„๋””์˜ค ๋‚ด ์žฅ๋ฉด)๋ฅผ ์ •์˜ํ•œ๋‹ค. ๋ณ„๋„์˜ ํ”„๋กœ์ ํŠธ ํŒŒ์ผ ํฌ๋งท์ด ์•„๋‹ˆ๋ผ plain HTML ์ž์ฒด๊ฐ€ ๋น„๋””์˜ค์˜ ๋‹จ์œ„์ด๋ฉฐ, data-* ์†์„ฑ์œผ๋กœ ํƒ€์ด๋ฐ๊ณผ ๋ ˆ์ด์–ด๋ง ์ •๋ณด๋ฅผ ์ถ”๊ฐ€ํ•œ ๊ฒƒ์ด๋‹ค.

ํ•„์ˆ˜ ๊ตฌ์กฐ

<div id="root" data-composition-id="my-video"
     data-start="0" data-width="1920" data-height="1080">
 
  <!-- ํƒ€์ด๋ฐ์ด ์ง€์ •๋œ ํด๋ฆฝ ์š”์†Œ -->
  <h1 id="title" class="clip"
      data-start="0" data-duration="5" data-track-index="0">
    Hello!
  </h1>
</div>

3๊ฐ€์ง€ ํ•ต์‹ฌ ๊ทœ์น™

  1. ๋ฃจํŠธ ์š”์†Œ์— data-composition-id, data-width, data-height ํ•„์ˆ˜
  2. ํƒ€์ด๋ฐ ์š”์†Œ์— data-start, data-duration, data-track-index, class="clip" ํ•„์ˆ˜
  3. GSAP ํƒ€์ž„๋ผ์ธ์€ { paused: true }๋กœ ์ƒ์„ฑํ•˜๊ณ  window.__timelines์— ๋“ฑ๋ก

ํด๋ฆฝ ํƒ€์ž… 4๊ฐ€์ง€

ํƒ€์ž…HTML ์š”์†Œ์šฉ๋„
Video<video>B-roll, A-roll ์˜์ƒ
Image<img>์ •์  ์˜ค๋ฒ„๋ ˆ์ด, ๊ทธ๋ž˜ํ”ฝ
Audio<audio>๋ฐฐ๊ฒฝ์Œ์•…, ํšจ๊ณผ์Œ
Nested Composition<div data-composition-src="...">์„œ๋ธŒ ์”ฌ, ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜

2-Layer ์•„ํ‚คํ…์ฒ˜

Composition์€ ๋‘ ๋ ˆ์ด์–ด๋กœ ๋ถ„๋ฆฌ๋œ๋‹ค:

  • HTML Layer (Primitives): ๋ฌด์—‡์ด ์–ธ์ œ ์–ด๋–ค ํŠธ๋ž™์—์„œ ์žฌ์ƒ๋˜๋Š”์ง€ ์„ ์–ธ โ€” data-* ์†์„ฑ์œผ๋กœ ์ œ์–ด
  • Script Layer (Effects): ์• ๋‹ˆ๋ฉ”์ด์…˜, ํŠธ๋žœ์ง€์…˜, ๋™์  DOM ์กฐ์ž‘ โ€” GSAP ํƒ€์ž„๋ผ์ธ์œผ๋กœ ๊ตฌํ˜„

์Šคํฌ๋ฆฝํŠธ์—์„œ ํด๋ฆฝ์˜ ํ‘œ์‹œ/์ˆจ๊น€์ด๋‚˜ ๋ฏธ๋””์–ด ์žฌ์ƒ์„ ์ง์ ‘ ์ œ์–ดํ•˜๋ฉด ์•ˆ ๋œ๋‹ค. ์ด๋Š” ํ”„๋ ˆ์ž„์›Œํฌ๊ฐ€ ์ž๋™์œผ๋กœ ๊ด€๋ฆฌํ•œ๋‹ค.

Frame Adapter โ€” ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋Ÿฐํƒ€์ž„ ์ถ”์ƒํ™”

Frame Adapter๋Š” โ€œํ”„๋ ˆ์ž„ N์—์„œ ํ™”๋ฉด์ด ์–ด๋–ป๊ฒŒ ๋ณด์—ฌ์•ผ ํ•˜๋Š”๊ฐ€?โ€๋ผ๋Š” ์งˆ๋ฌธ์— ๋‹ตํ•˜๋Š” ์ธํ„ฐํŽ˜์ด์Šค๋‹ค. ๋‹ค์–‘ํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋Ÿฐํƒ€์ž„(GSAP, Lottie, Three.js ๋“ฑ)์„ ํ†ต์ผ๋œ ๋ฐฉ์‹์œผ๋กœ ํ†ตํ•ฉํ•œ๋‹ค.

type FrameAdapter = {
  id: string;
  init?: (ctx: FrameAdapterContext) => Promise<void> | void;
  getDurationFrames: () => number;       // ์ด ํ”„๋ ˆ์ž„ ์ˆ˜ ๋ฐ˜ํ™˜
  seekFrame: (frame: number) => Promise<void> | void;  // ํ•ด๋‹น ํ”„๋ ˆ์ž„์œผ๋กœ ์ด๋™
  destroy?: () => Promise<void> | void;
};

๊ฒฐ์ •๋ก (Determinism) ๊ณ„์•ฝ

  • ์ •๊ทœ ์‹œ๊ณ„: t = frame / fps
  • Date.now() ๊ฐ™์€ wall-clock ์˜์กด ๊ธˆ์ง€
  • ์‹œ๋“œ ์—†๋Š” ๋žœ๋ค ๊ธˆ์ง€
  • ๋ Œ๋”๋ง ์ค‘ ๋„คํŠธ์›Œํฌ ์š”์ฒญ ๊ธˆ์ง€
  • seekFrame()์€ ๋ฉฑ๋“ฑ(idempotent) โ€” ๊ฐ™์€ ํ”„๋ ˆ์ž„ ์ž…๋ ฅ์ด๋ฉด ํ•ญ์ƒ ๋™์ผํ•œ ์ถœ๋ ฅ

์ง€์› ๋Ÿฐํƒ€์ž„ 6์ข…

๋Ÿฐํƒ€์ž„Seek ๋ฐฉ์‹
GSAPtimeline.totalTime(seconds)
Anime.jsinstance.seek(timeMs)
CSS KeyframesAnimation.currentTime
LottiegoToAndStop(timeMs, false)
Three.js/WebGLhf-seek ์ด๋ฒคํŠธ + window.__hfThreeTime
WAAPIanimation.currentTime

๋ Œ๋”๋ง ํŒŒ์ดํ”„๋ผ์ธ โ€” ํ”„๋ ˆ์ž„ ์บก์ฒ˜์—์„œ MP4๊นŒ์ง€

ํ•ต์‹ฌ ๊ณต์‹

frame = floor(time ร— fps)

๋ Œ๋”๋ง ํ๋ฆ„

  1. CLI๊ฐ€ Producer๋ฅผ ํ˜ธ์ถœ
  2. Producer๊ฐ€ Engine์„ ํ†ตํ•ด Headless Chrome ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ
  3. Composition HTML์„ Chrome์— ๋กœ๋“œ
  4. ๊ฐ ํ”„๋ ˆ์ž„์— ๋Œ€ํ•ด:
    • Frame Adapter์˜ seekFrame(N) ํ˜ธ์ถœ
    • Chrome beginFrame API๋กœ ํ•ด๋‹น ํ”„๋ ˆ์ž„ ์บก์ฒ˜ (PNG)
  5. ์บก์ฒ˜๋œ ํ”„๋ ˆ์ž„ ์‹œํ€€์Šค๋ฅผ FFmpeg์— ํŒŒ์ดํ”„
  6. FFmpeg๊ฐ€ x264๋กœ ์ธ์ฝ”๋”ฉ โ†’ MP4 ์ถœ๋ ฅ

๋ Œ๋”๋ง ๋ชจ๋“œ 2๊ฐ€์ง€

๋ชจ๋“œํŠน์„ฑ
Local์‹œ์Šคํ…œ Chrome + FFmpeg ์‚ฌ์šฉ, ๋น ๋ฆ„, GPU ๊ฐ€์† ๊ฐ€๋Šฅ
Docker๊ณ ์ •๋œ Chrome/FFmpeg/ํฐํŠธ, ํ”Œ๋žซํผ ๋ฌด๊ด€ ๋™์ผ ์ถœ๋ ฅ, CI/CD์šฉ

Quality ํ”„๋ฆฌ์…‹

ํ”„๋ฆฌ์…‹CRF์†๋„์šฉ๋„
Draft28ultrafast๋ฐ˜๋ณต ์ž‘์—…์šฉ ๋น ๋ฅธ ๋ฏธ๋ฆฌ๋ณด๊ธฐ
Standard18medium๊ธฐ๋ณธ๊ฐ’, 1080p์—์„œ ์‹œ๊ฐ์  ๋ฌด์†์‹ค
High15slow์ตœ์ข… ๋‚ฉํ’ˆ์šฉ

GSAP ํƒ€์ž„๋ผ์ธ โ€” ์• ๋‹ˆ๋ฉ”์ด์…˜์˜ ํ•ต์‹ฌ ํŒจํ„ด

Hyperframes์—์„œ GSAP ํƒ€์ž„๋ผ์ธ์€ ๋ฐ˜๋“œ์‹œ paused ์ƒํƒœ๋กœ ์ƒ์„ฑํ•˜๊ณ , ํ”„๋ ˆ์ž„์›Œํฌ๊ฐ€ ํ”„๋กœ๊ทธ๋ž˜๋งคํ‹ฑํ•˜๊ฒŒ ์Šคํฌ๋Ÿฌ๋น™ํ•œ๋‹ค:

// 1. paused ํƒ€์ž„๋ผ์ธ ์ƒ์„ฑ
const tl = gsap.timeline({ paused: true });
 
// 2. ์ ˆ๋Œ€ ์œ„์น˜ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋ฐฐ์น˜
tl.from("#title", { opacity: 0, y: -50, duration: 1 }, 0);   // 0์ดˆ๋ถ€ํ„ฐ
tl.to("#subtitle", { opacity: 1, duration: 0.5 }, 1.5);       // 1.5์ดˆ๋ถ€ํ„ฐ
 
// 3. composition-id์™€ ๋งค์นญํ•˜์—ฌ ๋“ฑ๋ก
window.__timelines = window.__timelines || {};
window.__timelines["my-video"] = tl;

Composition์˜ duration = GSAP ํƒ€์ž„๋ผ์ธ์˜ duration. ๋น„๋””์˜ค๊ฐ€ 30์ดˆ์ธ๋ฐ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด 8์ดˆ์— ๋๋‚˜๋ฉด ๋น„๋””์˜ค๋„ 8์ดˆ์—์„œ ์ž˜๋ฆฐ๋‹ค. ๋นˆ tween์œผ๋กœ ์—ฐ์žฅ:

tl.set({}, {}, 30);  // ํƒ€์ž„๋ผ์ธ์„ 30์ดˆ๋กœ ํ™•์žฅ

๊ฐœ๋ฐœ ์›Œํฌํ”Œ๋กœ์šฐ

# 1. ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ
npx hyperframes init my-video
 
# 2. ๋ธŒ๋ผ์šฐ์ €์—์„œ ์‹ค์‹œ๊ฐ„ ๋ฏธ๋ฆฌ๋ณด๊ธฐ
npx hyperframes preview
 
# 3. HTML ํŽธ์ง‘ (์—์ด์ „ํŠธ ๋˜๋Š” ์ง์ ‘)
# index.html ์ˆ˜์ • โ†’ ์ž๋™ ํ•ซ ๋ฆฌ๋กœ๋“œ
 
# 4. ๋ฆฐํŠธ ๊ฒ€์‚ฌ
npx hyperframes lint
 
# 5. MP4 ๋ Œ๋”๋ง
npx hyperframes render --output output.mp4

Hyperframes vs Remotion

HyperframesRemotion
์ €์ž‘ ๋ฐฉ์‹HTML + CSS + seekable ์• ๋‹ˆ๋ฉ”์ด์…˜React ์ปดํฌ๋„ŒํŠธ
๋นŒ๋“œ ์Šคํ…์—†์Œ โ€” index.html์ด ๊ณง ํ”Œ๋ ˆ์ด ๊ฐ€๋Šฅ๋ฒˆ๋“ค๋Ÿฌ ํ•„์ˆ˜
์—์ด์ „ํŠธ ํ•ธ๋“œ์˜คํ”„Plain HTML ํŒŒ์ผJSX / React ํ”„๋กœ์ ํŠธ
๋ผ์ด์„ ์ŠคApache 2.0Remotion License (source-available)
๋ถ„์‚ฐ ๋ Œ๋”๋งLocal + AWS LambdaRemotion Lambda

์ฐธ๊ณ  ๋ฌธ์„œ