vid-product-launch
제품 설명과 출시 컨텍스트에서 내러티브 MP4 제품 출시 비디오를 생성합니다. 파이프라인: HTML/CSS 애니메이션 → 헤드리스 Chromium (Playwright, 프레임별) → FFmpeg (H.264 MP4). React 없음. AI 비디오 API 없음. Python 없음. Playwright + FFmpeg 외에 추가 비용 없음.
중요 규칙 (모든 생성 전에 읽을 것)
-
태그라인은 선택 사항이 아닙니다. 건너뛰지 마세요. 태그라인은 제품의 전체 약속을 4~6단어로 표현한 것입니다. 사용자가 제공하지 않은 경우 설명에서 하나를 도출하세요. 날카롭고 능동적인 어조로 압축해야 합니다. 절대로 "[tagline here]" 또는 빈 칸으로 두지 마세요.
-
공개 순간은 정당한 느낌을 줘야 합니다. 애타게 하기와 구축 섹션은 긴장감을 조성하기 위해 존재합니다. 비디오의 처음 20%에 제품명이 나타나면 내러티브가 무너집니다. 절대로 공개 섹션 전에 제품명을 드러내지 마세요.
-
product_name 글꼴 크기: 16:9의 경우 최소 120px, 9:16의 경우 최소 80px. 공개 순간에 제품명이 가장 큰 요소여야 합니다. 지배적이지 않으면 공개가 실패합니다.
-
하나의 증거 통계. 목록이 아닙니다. 사용자가 여러 통계를 제공하면 가장 강력한 것을 선택하세요. 5개의 숫자 목록은 충격을 파괴합니다. 하나의 큰 숫자가 충격을 만듭니다.
-
window.renderFrame(t)를 사용하세요 — 장면 전환에 CSS@keyframes를 사용하지 마세요. CSScurrentTime탐색은 Chromium에서 역방향 탐색에 대해 조용히 무시됩니다. renderFrame 함수는 밀리초에서 직접 요소 스타일을 계산합니다. Playwright는 프레임당 한 번 호출합니다. -
어떤 요소에도
animation-delay를 사용하지 마세요. renderFrame에서는 필요하지 않습니다.animation-delay를 작성한다면 잘못된 아키텍처를 사용하고 있는 것입니다. -
window.__videoReady = true는 오직document.fonts.ready.then(...)내부에만 설정하세요. 절대로 동기적으로 설정하지 마세요. -
window.__stopPreview()를 노출하세요. rAF 미리보기 루프는 Playwright의 evaluate/screenshot 호출과 경쟁합니다. 캡처 스크립트는 프레임 루프 전에__stopPreview()를 호출합니다. -
장면 경계 확인 시
t < startMs를 사용하세요 (t <= startMs가 아님). 프레임 0에서t <= 0은 장면 1을 검게 만듭니다. -
Body = 정확한 픽셀 치수. 너비와 높이는 정수입니다.
%,vw/vh,rem을 사용하지 마세요. -
HTML을 생성하기 전에
references/scene-library.md와references/style-presets.md를 반드시 읽으세요. -
절대로 채팅에 HTML을 덤프하지 마세요. 파일로 저장하세요. 요약만 표시하세요.
-
필름 그레인 캔버스는 반드시 240×135이어야 하며 W×H가 아닙니다. 캔버스 요소에
width="240" height="135"를 설정한 다음style="width:[W]px;height:[H]px"로 늘리세요. 1920×1080에서 전체 해상도의 그레인은 64배 느리며 프레임당 8MB의 ImageData를 생성하여 1800프레임 내보내기에 몇 시간이 걸립니다. -
애타게 하기 섹션은 톤과 관계없이 반드시 어두워야 합니다 (#000 또는 거의 검정색). 애타게 하기 섹션의 흰색 배경은 데모 슬라이드처럼 읽히며 제품 출시 비디오처럼 보이지 않습니다. 어두움에서 밝음으로의 내러티브 호 (어두운 애타게 → 어두운 구축 → 제품 공개)는 출시 비디오가 드라마를 만드는 방식입니다.
minimal및energetic프리셋조차도 애타게-문제 및 긴장-구축 장면에background: #000을 사용해야 합니다. -
각 애타게 단어는 자체 비트여야 하며 200px 이상의 글꼴 크기를 가져야 합니다. 한 줄에 단어별로 배치하는 것은 충분히 펀치가 있지 않습니다. 각 문제 단어(예: "연구." / "작성." / "홍보." / "반복.")는 1500–1800ms의 화면 시간을
font-size: 200px; font-weight: 900으로 가운데 정렬하여 하나씩 표시합니다. scene-library.md의tease-words패턴을 참조하세요. -
구축 섹션은 문제를 서술하는 콘텐츠를 표시해야 합니다 — 입자만 있는 것이 아닙니다.
terminal-card패턴을 사용하세요: 수동 작업이 입력되는 타자기 애니메이션과 함께 3개의 카드가 순차적으로 나타납니다. 카드에는border: 1px solid rgba(255,255,255,0.07), 파란 점 악센트, 고정폭 텍스트가 있습니다. 20초 동안 입자만으로는 빈 화면 시간입니다. -
어두운 장면에 도트 그리드 CSS 배경을 추가하세요. 빌드, 공개, 증거 및 CTA 장면에
background-image: radial-gradient(circle, rgba(255,255,255,0.035) 1px, transparent 1px); background-size: 60px 60px;를 추가하세요. 계산 비용이 없으며 깊이를 더해줍니다.
1단계: 입력
필수:
product_name— 출시되는 제품 또는 기능의 이름product_description— 2~3문장: 무엇을 하는지, 누구를 위한 것인지, 주요 이점
선택적 매개변수 및 기본값:
| 매개변수 | 기본값 | 설명 |
|---|---|---|
| tagline | 자동 | 4~6단어 — 제품의 핵심 약속 |
| problem_statement | 자동 추론 | 애타게 하기 섹션용 1문장 (제품이 해결하는 문제) |
| proof_stat | 자동 추론 | 단일 지표 (예: "500+ 팀", "10× 더 빠름", "$2M 절약") |
| cta | 자동 | URL 또는 행동 문구 (예: "opendirectory.dev", "대기자 명단에 가입하세요") |
| launch_date | — | ISO 날짜 문자열 (카운트다운 카드 장면 활성화) |
| tone | cinematic | cinematic / energetic / minimal / emotional |
| duration | 60 | 30 / 60 / 90 (초) |
| aspect_ratio | 16:9 | 16:9 (1920×1080) / 9:16 (1080×1920) |
| letterbox | false | 영화적 2.35:1 검은 막대 — 16:9 전용 |
| music | — | 오디오 파일 경로 (mp3/m4a/wav) |
| fps | 30 | 초당 프레임 수 (24, 30 또는 60) |
product_name 또는 product_description이 누락된 경우 정확히 다음과 같이 질문하세요:
"출시 비디오를 만들려면 두 가지가 필요합니다:
- 제품 이름 — 제품의 이름은 무엇입니까?
- 제품 설명 — 2~3문장: 무엇을 하는지, 누구를 위한 것인지, 주요 이점은 무엇입니까?
선택 사항: 태그라인 (4~6단어), 증거 통계 (하나의 숫자), CTA (URL 또는 행동 문구), 톤 (cinematic / energetic / minimal / emotional), 기간 (30 / 60 / 90초)."
두 가지 모두 있으면 즉시 2단계로 진행하세요.
2단계: 내부 아키텍처 (사용자에게 절대 표시하지 않음)
1. 누락된 매개변수 도출:
- tagline (제공되지 않은 경우): product_description에서 하나를 작성하세요. 4~6단어. 능동적 어조. 불필요한 내용 없음 ("미래의…", "소개합니다…"). 가장 날카로운 약속을 도출하세요.
- problem_statement (제공되지 않은 경우): product_description에서 추론하세요. 고통을 진술하는 1문장. 제품명을 언급하지 않음. 청중이 인식하며 고개를 끄덕이게 작성하세요.
- proof_stat (제공되지 않은 경우): product_description에서 추론하세요. 통계를 사용할 수 없으면 신뢰할 수 있는 규모 표시기를 사용하세요 ("500+ 팀이 신뢰함", "며칠에서 몇 분으로").
- cta (제공되지 않은 경우): product_name에서 추론 가능하면 제품 도메인을 사용하거나 행동 문구로 "자세히 알아보기"를 사용하세요.
2. duration에서 섹션 타이밍 계산:
| 섹션 | 30초 | 60초 | 90초 | 시작 공식 |
|---|---|---|---|---|
| Tease | 0–5초 | 0–10초 | 0–15초 | 0ms |
| Build | 5–12초 | 10–30초 | 15–40초 | Tease 종료 |
| Reveal | 12–20초 | 30–45초 | 40–60초 | Build 종료 |
| Proof | 20–25초 | 45–55초 | 60–75초 | Reveal 종료 |
| CTA | 25–30초 | 55–60초 | 75–90초 | Proof 종료 |
모든 경계를 밀리초로 변환하세요. 상수에 할당:
TEASE_START_MS, TEASE_END_MS
BUILD_START_MS, BUILD_END_MS
REVEAL_START_MS, REVEAL_END_MS
PROOF_START_MS, PROOF_END_MS
CTA_START_MS, CTA_END_MS
3. 섹션별 장면 선택:
- Tease: 항상
blackout-opener(1500ms)로 시작합니다. 그런 다음tease-words(각 문제 키워드를 200px+에서 1600–1800ms 비트로).launch_date가 제공되면 Tease 끝에 선택적으로countdown-card를 추가합니다. - Build:
terminal-card시퀀스 (수동 작업이 입력되는 것을 보여주는 3개의 카드) + 선택적 종료 악센트 라인. 기간 ≥ 30초는 3개의 카드를 모두 사용합니다. 기간 = 30초는 2개의 카드를 사용합니다. 5초 이상 동안 입자만 사용하지 마세요 — 화면은 문제를 서술하는 콘텐츠를 표시해야 합니다. - Reveal:
reveal-hero— 항상 이 섹션의 첫 번째입니다. 기간 ≥ 60초인 경우 선택적으로tagline-card가 뒤따릅니다. - Proof: 숫자 통계가 있으면
proof-stat. 그렇지 않으면feature-bullet. - CTA:
cta-card— 항상, 항상 마지막입니다.
일반적인 장면 수: 30초 = 4개 장면, 60초 = 67개 장면, 90초 = 78개 장면.
4. 장면별 타이밍 계산:
각 섹션 내에서 해당 섹션의 장면 간에 시간을 균등하게 나눕니다.
예외: blackout-opener는 항상 Tease의 처음 1500ms를 정확히 차지합니다.
5. 픽셀 치수 결정:
16:9→ W=1920, H=10809:16→ W=1080, H=1920
6. 레터박스 계산 (활성화된 경우, 16:9 전용):
- 2.35:1 콘텐츠 높이 = W / 2.35 = 1920 / 2.35 ≈ 817px
- 막대 높이 = (H - 817) / 2 = (1080 - 817) / 2 ≈ 132px
- 상단 막대:
top: 0; height: 132px - 하단 막대:
bottom: 0; height: 132px
7. 영화적 효과 플래그 (프리셋별):
cinematic: 필름 그레인=ON (캔버스 240×135, 불투명도 0.025), 비네트=ON, 라이트 릭=ON (따뜻한 골드), 도트 그리드=ON (어두운 장면)energetic: 필름 그레인=OFF, 비네트=OFF, 도트 그리드=ON, 공개 시 흰색 플래시minimal: 필름 그레인=ON (캔버스 240×135, 불투명도 0.022), 비네트=ON (미묘함, 0.5 방사형), 도트 그리드=ON, 라이트 릭 없음 — 악센트 색상 = #4B9FFF (일렉트릭 블루)emotional: 필름 그레인=ON (캔버스 240×135, 불투명도 0.018, 따뜻한 색조 블렌드), 비네트=ON (부드러움), 라이트 릭=ON (구리 따뜻함), 도트 그리드=OFF
모든 프리셋: 애타게 및 구축 장면은 프리셋에 관계없이 항상 어두운 배경 (#000 또는 거의 검정색)을 사용합니다.
8. HTML에 window.__sfxTimeline을 포함 (항상 — 음악이 제공되지 않은 경우에도):
내보내기 스크립트는 이 배열을 읽고 각 SFX 유형을 FFmpeg aevalsrc/anoisesrc로 기본 합성하여 adelay를 사용하여 정확한 밀리초 오프셋에 이벤트를 배치합니다. 외부 오디오 파일이 필요하지 않습니다.
SFX 유형 참조:
| 유형 | 사운드 | 지속 시간 | 참고 사항 |
|---|---|---|---|
word-hit | 서브 펀치 (50Hz) + 과도 클릭 (2.2kHz) + 노이즈 버스트 | 180ms | 애타게 단어당 하나 — amix를 통해 3개 레이어 |
type-sequence | 15Hz의 기계식 키보드 클릭 (sin^30 펄스 엔벨로프) | 1.9초 | 카드당 하나 — 전체 TYPE_DUR 동안 실행 |
whoosh | 2밴드 노이즈 스윕 (1.1kHz 바디 + 4–8kHz 에어) | 700ms | BUILD_CLOSE 전환 시 |
tension-riser | 낮은 럼블 (250Hz) + t/2.8 램프로 증가하는 서브 톤 | 2.9초 | BUILD_CLOSE + 700ms에서 시작 — REVEAL_START에서 절정 |
reveal-boom | 서브 (45Hz) + 바디 (90Hz) + 샤이머 (4.5–11kHz) + 85ms 에코 | 900ms | 정확히 REVEAL_START에서 — THE 히트 |
counter-tick | 고조파 클릭 (880Hz + 440Hz + 1760Hz + 2640Hz) | 80ms | 3비트 — 감소 0.32 → 0.26 → 0.20 |
cta-chime | A 메이저 코드 (440 + 554 + 659 + 880Hz) + aecho 벨 샤이머 | 1.2초 | 정확히 CTA_START에서 |
SFX 타이밍 맵 — 섹션 상수에서 ms 값을 계산하고 절대 하드코딩하지 마세요:
window.__sfxTimeline = [
// ── TEASE: 단어 비트당 하나의 word-hit, 마지막이 더 큼 ───────────────────────
{ ms: WORD_BEATS[0].start, sfx: 'word-hit', vol: 0.55 },
{ ms: WORD_BEATS[1].start, sfx: 'word-hit', vol: 0.55 },
{ ms: WORD_BEATS[2].start, sfx: 'word-hit', vol: 0.55 },
{ ms: WORD_BEATS[N].start, sfx: 'word-hit', vol: 0.65 }, // 마지막 단어가 더 큼
// ── BUILD: 카드당 type-sequence (1.9초, TYPE_DUR 1800ms와 일치) ────────
{ ms: CARDS[0].start + 180, sfx: 'type-sequence', vol: 0.28 },
{ ms: CARDS[1].start + 180, sfx: 'type-sequence', vol: 0.28 },
{ ms: CARDS[2].start + 180, sfx: 'type-sequence', vol: 0.28 },
// ── TRANSITION: whoosh 후 2.9초 상승, REVEAL_START에서 절정 ───────────
{ ms: BUILD_CLOSE, sfx: 'whoosh', vol: 0.50 },
{ ms: BUILD_CLOSE + 700, sfx: 'tension-riser', vol: 0.35 },
// ── REVEAL: 영화적 히트 ─────────────────────────────────────────────
{ ms: REVEAL_START, sfx: 'reveal-boom', vol: 0.88 },
// ── PROOF: 고조파 틱, 감소 ───────────────────────────────────
{ ms: PROOF_START, sfx: 'counter-tick', vol: 0.32 },
{ ms: PROOF_START + 2200, sfx: 'counter-tick', vol: 0.26 },
{ ms: PROOF_START + 4400, sfx: 'counter-tick', vol: 0.20 },
// ── CTA: A 메이저 코드 착지 ────────────────────────────────────────────
{ ms: CTA_START, sfx: 'cta-chime', vol: 0.62 },
];
이 블록을 미리보기 루프 바로 앞, window.renderFrame 뒤에 배치하세요.
3단계: HTML 생성
코드를 작성하기 전에 references/scene-library.md와 references/style-presets.md를 읽으세요.
해당 파일의 정확한 CSS 클래스 이름, HTML 구조 및 renderFrame 패턴을 사용하세요.
필수 HTML 스켈레톤:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
[스타일 프리셋의 글꼴 CDN 링크]
<style>
:root {
[스타일 프리셋의 모든 CSS 토큰]
}
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
width: [W]px; height: [H]px;
overflow: hidden;
background: var(--bg);
font-family: var(--font-body);
position: relative;
}
.scene {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px;
opacity: 0;
will-change: opacity, transform;
}
.scene-inner {
width: 100%;
max-width: 1200px;
}
[scene-library.md의 각 장면 유형에서 CSS 붙여넣기]
</style>
</head>
<body>
<!-- 모든 장면 위에 렌더링되는 영화적 오버레이 요소 -->
[필름 그레인: ON인 경우]
<canvas id="grain-overlay"
width="[W]" height="[H]"
style="position:fixed;inset:0;pointer-events:none;opacity:0.025;mix-blend-mode:overlay;z-index:50"></canvas>
[종료]
[비네트: ON인 경우]
<div id="vignette-overlay"
style="position:fixed;inset:0;background:radial-gradient(ellipse at center,transparent 35%,rgba(0,0,0,0.65) 100%);pointer-events:none;z-index:51"></div>
[종료]
[라이트 릭: ON인 경우]
<div id="light-leak"
style="position:fixed;inset:0;background:linear-gradient(135deg,rgba(255,220,140,0.5) 0%,rgba(255,255,255,0.3) 45%,transparent 70%);opacity:0;pointer-events:none;z-index:52"></div>
[종료]
[레터박스: ON인 경우]
<div id="lbox-top" style="position:fixed;top:0;left:0;width:[W]px;height:132px;background:#000;z-index:100"></div>
<div id="lbox-bot" style="position:fixed;bottom:0;left:0;width:[W]px;height:132px;background:#000;z-index:100"></div>
[종료]
<!-- 장면 -->
[scene-library.md 템플릿의 장면 HTML, 선택된 장면당 하나]
<script>
window.__videoReady = false;
window.TOTAL_DURATION_MS = [duration * 1000];
// ── 섹션 타이밍 상수 ──────────────────────────────────────────────────
const TEASE_START_MS = [N];
const TEASE_END_MS = [N];
const BUILD_START_MS = [N];
const BUILD_END_MS = [N];
const REVEAL_START_MS = [N];
const REVEAL_END_MS = [N];
const PROOF_START_MS = [N];
const PROOF_END_MS = [N];
const CTA_START_MS = [N];
const CTA_END_MS = [N];
// ── 애타게 단어 비트 ───────────────────────────────────────────────────
const WORDS = ["[word1]", "[word2]", "[word3]", "[word4]"];
const WORD_DUR = 1600; // 각 단어의 ms
const WORD_GAP = 100; // 단어 간 간격 ms
const WORD_BEATS = WORDS.map((w, i) => ({
start: TEASE_START_MS + 1500 + i * (WORD_DUR + WORD_GAP),
end: TEASE_START_MS + 1500 + i * (WORD_DUR + WORD_GAP) + WORD_DUR
}));
// ── 터미널 카드 ───────────────────────────────────────────────────────
const CARD_DUR = 1800;
const CARD_GAP = 300;
const CARDS = [
{ start: BUILD_START_MS, label: "$ [command1]", lines: ["[line1a]", "[line1b]", "[line1c]"] },
{ start: BUILD_START_MS + (CARD_DUR + CARD_GAP), label: "$ [command2]", lines: ["[line2a]", "[line2b]", "[line2c]"] },
{ start: BUILD_START_MS + 2 * (CARD_DUR + CARD_GAP), label: "$ [command3]", lines: ["[line3a]", "[line3b]", "[line3c]"] }
];
const BUILD_CLOSE = CARDS[2].start + CARD_DUR + 500;
// ── renderFrame: 모든 장면 전환 및 애니메이션 처리 ───────────────────────
function renderFrame(t) {
// ... (scene-library.md 패턴 사용)
}
// ── SFX 타임라인 ───────────────────────────────────────────────────────
window.__sfxTimeline = [
{ ms: WORD_BEATS[0].start, sfx: 'word-hit', vol: 0.55 },
{ ms: WORD_BEATS[1].start, sfx: 'word-hit', vol: 0.55 },
{ ms: WORD_BEATS[2].start, sfx: 'word-hit', vol: 0.55 },
{ ms: WORD_BEATS[3].start, sfx: 'word-hit', vol: 0.65 },
{ ms: CARDS[0].start + 180, sfx: 'type-sequence', vol: 0.28 },
{ ms: CARDS[1].start + 180, sfx: 'type-sequence', vol: 0.28 },
{ ms: CARDS[2].start + 180, sfx: 'type-sequence', vol: 0.28 },
{ ms: BUILD_CLOSE, sfx: 'whoosh', vol: 0.50 },
{ ms: BUILD_CLOSE + 700, sfx: 'tension-riser', vol: 0.35 },
{ ms: REVEAL_START, sfx: 'reveal-boom', vol: 0.88 },
{ ms: PROOF_START, sfx: 'counter-tick', vol: 0.32 },
{ ms: PROOF_START + 2200, sfx: 'counter-tick', vol: 0.26 },
{ ms: PROOF_START + 4400, sfx: 'counter-tick', vol: 0.20 },
{ ms: CTA_START, sfx: 'cta-chime', vol: 0.62 },
];
// ── 미리보기 루프 ───────────────────────────────────────────────────────
window.__stopPreview = (function() {
let running = true;
function frame() {
if (!running) return;
renderFrame(Date.now() % TOTAL_DURATION_MS);
requestAnimationFrame(frame);
}
frame();
return function() { running = false; };
})();
// ── 비디오 준비 신호 ─────────────────────────────────────────────────────
document.fonts.ready.then(() => {
window.__videoReady = true;
});
</script>
</body>
</html>
4단계: 내보내기 (사용자용)
4a. 파일 생성 (권장)
index.html과 선택적 오디오 파일을 저장합니다. 요약 표시:
파일 생성됨:
- 📄
index.html(XX줄)- [...] 선택적
4b. 렌더링 명령어 (사용자에게 제공할 것)
세 가지 방법:
방법 1 — 직접 다운로드 (빠름): 파일을 브라우저에서 엽니다. 페이지 로드 시 재생됩니다. window.__videoReady가 참이 될 때까지 기다렸다가 renderFrame을 수동으로 폴링하거나 Playwright로 캡처합니다.
방법 2 — Playwright (권장): capture.js 스크립트를 사용합니다:
const { chromium } = require('playwright');
const { spawn } = require('child_process');
const fs = require('fs');
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1920, height: 1080 } });
await page.goto('file://' + __dirname + '/index.html');
await page.waitForFunction(() => window.__videoReady);
await page.evaluate(() => window.__stopPreview && window.__stopPreview());
const fps = 30;
const totalFrames = fps * 60; // 60초
const dir = './frames';
if (!fs.existsSync(dir)) fs.mkdirSync(dir);
for (let i = 0; i < totalFrames; i++) {
const t = Math.round(i * 1000 / fps);
await page.evaluate(`renderFrame(${t})`);
await page.screenshot({ path: `${dir}/frame_${String(i).padStart(6, '0')}.png` });
}
await browser.close();
const ffmpeg = spawn('ffmpeg', [
'-framerate', `${fps}`,
'-i', `${dir}/frame_%06d.png`,
'-c:v', 'libx264',
'-pix_fmt', 'yuv420p',
'-preset', 'medium',
'-crf', '18',
'-y', 'output.mp4'
]);
ffmpeg.stderr.on('data', d => process.stderr.write(d));
ffmpeg.on('close', () => console.log('✅ output.mp4 ready'));
})();
방법 3 — FFmpeg 이미지 시퀀스: 프레임을 PNG로 저장한 다음:
ffmpeg -framerate 30 -i frames/frame_%06d.png -c:v libx264 -pix_fmt yuv420p -preset medium -crf 18 -y output.mp4
5단계: (선택 사항) 음악 통합
파이프라인 마지막에 오디오 파일을 MP4에 믹싱:
ffmpeg -i output.mp4 -i music.mp3 -c:v copy -c:a aac -map 0:v:0 -map 1:a:0 -shortest -y final_with_audio.mp4
SFX는 여전히 필요합니다. 음악은 분위기를 더합니다.