자막 애니메이션 (Caption Animation)
음성 대본이나 나레이션을 단어별로 정확하게 맞춘, 시선을 사로잡는 자막으로 변환합니다. 각 단어가 음절에 맞춰 튀어나오고, 현재 재생 중인 단어는 하이라이트되며, 텍스트는 나레이션에 딱 맞게 고정됩니다. TikTok, Reels, Shorts에서 시청 시간을 높이는, 읽기 좋고 리듬에 맞으며 안전 영역 안에 배치된 자막을 만듭니다.
사용 시기
- 숏폼 수직 영상(9:16)에 단어별 또는 노래방 스타일 자막을 넣을 때
- 음성 대본을 기반으로 한 음성/나레이션 트랙에 오픈 자막을 입힐 때
- Whisper/SRT 대본을 Remotion 또는 웹에서 애니메이션 텍스트로 변환할 때
- 자동 생성 자막을 브랜드화된 'Hormozi 스타일'로 재디자인할 때
파이프라인
| 단계 | 작업 | 도구 |
|---|---|---|
| 전사(Transcribe) | 오디오 → 단어 수준 타임스탬프 | Whisper (@remotion/install-whisper-cpp), AssemblyAI |
| 정규화(Normalize) | → { text, startMs, endMs }[] 토큰 | @remotion/captions Caption 타입 |
| 페이지화(Page) | 단어를 1~4개 단어 그룹으로 묶기 | createTikTokStyleCaptions() |
| 애니메이션(Animate) | 단어별 팝인 + 현재 단어 하이라이트 | Remotion spring() / CSS |
| 배치(Place) | 안전 영역, 가독성 좋은 타이포그래피 | 아래 레이아웃 규칙 참조 |
| 내보내기(Export) | 인코딩(MP4) 및/또는 보조 SRT/VTT 파일 | Remotion 렌더 / 파일 생성 |
두 가지 불가침 규칙
- 단어 수준 타이밍, 줄 수준이 아님. 노래방 자막은 각 단어가 음절에 정확히 맞을 때만 마법처럼 느껴집니다. 항상 단어 타임스탬프를 추출하고, 줄 지속 시간을 균등하게 나누어 가짜로 만들지 마십시오. 불일치는 즉시 눈에 띕니다.
- 가독성이 스타일보다 우선합니다. 자막은 햇빛 아래, 음소거 상태의 휴대폰에서 읽힙니다. 굵은 산세리프, 두꺼운 테두리 또는 그림자, 높은 대비가 먼저입니다. 장식은 그 다음입니다. 아무도 읽을 수 없는 자막은 자막이 아니라 장식일 뿐입니다.
대본에서 단어 타이밍 얻기
Whisper는 단어 수준 타임스탬프를 직접 제공합니다. 모든 소스를 하나의 평평한 토큰 형태로 정규화하여 렌더러가 단어의 출처를 신경 쓰지 않도록 합니다:
// 자막 토큰 — 다운스트림에서 소비하는 유일한 형태
type Token = { text: string; startMs: number; endMs: number };
// SRT 큐 블록 "00:00:01,200 --> 00:00:01,640"을 ms로 파싱
const toMs = (t: string) => {
const [h, m, rest] = t.split(":");
const [s, ms] = rest.split(",");
return ((+h * 60 + +m) * 60 + +s) * 1000 + +ms;
};
문장 수준 큐만 있다면 보간하지 말고 단어 타이밍을 위해 다시 전사하십시오. Whisper의 medium.en은 large보다 약 2배 빠르며 깨끗한 음성 오디오에 대해 충분히 정확합니다.
단어별 팝인 (Remotion)
각 단어의 크기와 불투명도를 자체 startMs에 고정된 스프링으로 구동합니다. 미세한 오버슈트가 "살아있는" 느낌을 줍니다.
import { useCurrentFrame, useVideoConfig, spring, interpolate } from "remotion";
const Word: React.FC<{ token: Token; active: boolean }> = ({ token, active }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const enter = (token.startMs / 1000) * fps; // 단어 자체의 시작 프레임
const p = spring({ frame: frame - enter, fps, config: { damping: 12, mass: 0.6 } });
const scale = interpolate(p, [0, 1], [0.6, 1]); // 60% → 100%로 팝인 (오버슈트)
return (
<span style={{
display: "inline-block",
transform: `scale(${scale})`,
opacity: interpolate(p, [0, 1], [0, 1]),
color: active ? "#FFE45E" : "#FFFFFF", // 현재 단어 하이라이트
transition: "color 80ms linear",
}}>{token.text} </span>
);
};
active는 frame을 각 토큰의 [startMs, endMs]에 대해 테스트하여 계산합니다. 하이라이트는 단일 악센트 색상 또는 현재 단어 뒤의 채워진 "상자"로 유지하고, 모든 단어의 색상을 한 번에 애니메이션하지 마십시오.
텍스트 벽이 아닌 페이지
한 번에 1~4개의 단어를 표시합니다. Remotion의 createTikTokStyleCaptions()를 사용하거나 수동으로 그룹화하고 combineTokensWithinMilliseconds를 조정하십시오: 진정한 단어별 효과는 ~200–500ms, 짧은 읽을 수 있는 구문은 ~1000–1500ms.
import { createTikTokStyleCaptions } from "@remotion/captions";
const { pages } = createTikTokStyleCaptions({
captions, combineTokensWithinMilliseconds: 1200, // 페이지당 ~2–3개 단어
});
웹/CSS 팝인 (Remotion 없이)
DOM/라이브 플레이어의 경우 각 단어에 startMs와 동일한 animation-delay를 지정하십시오:
.word { display:inline-block; opacity:0; animation: pop .26s cubic-bezier(.34,1.56,.64,1) forwards; }
@keyframes pop { from { opacity:0; transform:translateY(.18em) scale(.7) } to { opacity:1; transform:none } }
.word.active { color:#FFE45E; }
words.forEach(w => { const el = mk(w.text); el.style.animationDelay = `${w.startMs}ms`; track.append(el); });
가독성 좋은 타이포그래피 및 안전 영역 배치 (9:16, 1080×1920)
| 속성 | 값 | 이유 |
|---|---|---|
| 폰트 | Bold/ExtraBold 산세리프 (Montserrat, Inter, Helvetica) | 작고 복잡한 화면에서 읽기 쉬움 |
| 크기 | 56–80px (화면 높이의 약 8%), 최소 45px | 휴대폰에서 음소거 상태로도 읽기 가능 |
| 테두리(Stroke) | 2–6px 검은색 실선 외곽선 | 어떤 배경에서도 선명함 유지 |
| 그림자(Shadow) | 테두리 백업으로 부드러운 드롭 섀도우 | 밝은 화면에서 분리감 제공 |
| 대소문자 | 대문자 또는 문장; 높은 대비 채우기 | 임팩트 + 스캔 용이성 |
| 수직 위치 | 중앙 대역, 아래쪽 약 62~70% | UI 위, 액션 아래 |
| 하단 안전 | 아래쪽 약 280px / 15% 확보 | 자막/CTA/오디오 UI와 겹침 방지 |
| 측면 안전 | 중앙 80% 너비 내 유지 | 오른쪽 레일 아이콘과 겹침 방지 |
자막을 중앙 대역에 배치하십시오 — 플랫폼 UI(사용자 이름, 오디오, 버튼)가 있는 맨 아래에 고정하지 마십시오. 텍스트는 최대 약 2줄로 패딩 처리하십시오.
음성/나레이션과 동기화
대본에는 이미 정확한 오디오의 타이밍이 포함되어 있습니다. 동일한 오디오를 컴포지션 아래에 배치하고 타임스탬프를 오디오의 시간 기준으로 유지하십시오 — 자막과 음성은 수동 조정 없이 잠긴 상태를 유지합니다. 보이스오버가 다시 녹음된 경우 다시 전사하십시오. 오프셋을 수동으로 이동하지 마십시오.
인코딩 vs 보조 SRT/VTT
| 인코딩 (오픈) | 보조 SRT/VTT (클로즈드) | |
|---|---|---|
| 위치 | MP4 내 픽셀 | 별도 .srt/.vtt 파일 |
| 소셜 (TikTok/Reels) | 필수 — 보장됨, 스타일 지정 가능 | 플랫폼이 무시하는 경우가 많음 |
| 접근성 (ADA/WCAG) | 단독으로 충족하지 못함 | 필수 (토글 가능) |
| 모범 사례 | 소셜용으로 애니메이션 자막 인코딩 | 웹/SEO를 위해 보조 파일도 제공 |
둘 다 내보내십시오: 소셜용으로 애니메이션 자막을 인코딩하고, 동일한 토큰에서 일반 SRT/VTT를 작성하여 접근성 및 SEO 재생을 지원하십시오.
출력 체크리스트
- 단어 수준 타임스탬프 (실제, 전사 결과) — 균등 분할 가짜 없음.
- 미세 오버슈트가 있는 단어별 팝인, 현재 단어에 하나의 악센트 색상.
- 페이지당 1~4개 단어; 텍스트 벽 없음.
- 굵은 산세리프, 56–80px, 2–6px 테두리 + 그림자, 높은 대비.
- 중앙 대역, 아래쪽 약 280px 및 측면 레일과 겹치지 않음.
- 오디오가 컴포지션 아래에 배치됨; 타임스탬프는 오디오 시간 기준.
- 소셜용 인코딩 자막 + 접근성용 보조 SRT/VTT 제공.
전달 및 확인 (렌더링된 스틸 → MP4)
패키지된 헬퍼 (
scripts/):scripts/contact-sheet.sh sheet.png f-hook.png f-mid.png f-end.png로 스틸을 타일링한 다음scripts/probe-mp4.sh out.mp4 [WxH] [fps]로 인코딩을 확인하십시오.scripts/README.md를 참조하십시오.
자막은 Remotion 컴포지션 (<Composition> + zod schema + defaultProps)으로 제공됩니다 — 모든 단어 모션은 useCurrentFrame()에 의해 프레임 기반으로 구동되며, Date.now() / Math.random() / 타이머를 사용하지 않습니다. 전달물 = out/*.mp4 (인코딩됨) + 프로젝트 + 보조 SRT/VTT. 9:16 수직(1080×1920)이 기본값입니다.
확인 루프 — 스틸 렌더링 → 검사 → 인코딩. 단어 타이밍이 깨지기 쉬운 부분입니다. 인코딩하기 전에 정확한 프레임에서 확인하십시오.
# 시작 / 샘플링된 현재 단어 프레임 / 끝 스틸 — 제공된 프롭스(실제 토큰 + 오디오) 사용
npx remotion still Captions out/f-start.png --frame=0 --props='{"captionsSrc":"vo.json"}'
npx remotion still Captions out/f-mid.png --frame=90 --props='{"captionsSrc":"vo.json"}'
npx remotion still Captions out/f-end.png --frame=N --props='{"captionsSrc":"vo.json"}' # N = durationInFrames-1
# 각 PNG 검사:
# - 프레임 90에서 하이라이트된 단어는 [startMs,endMs]가 90/fps를 포함하는 단어여야 함 (불일치 없음)
# - 인코딩 자막 가독성: 굵은 산세리프, 테두리+그림자 유지, 잘림 없음
# - 9:16 안전 영역: 자막이 중앙 대역에 위치, 위쪽 약 12% 및 아래쪽 약 20-35% (자막/CTA/오디오 UI)와 오른쪽 액션 레일에서 벗어나 있어야 함
npx remotion render Captions out/captions.mp4 --props='{"captionsSrc":"vo.json"}' # 스틸이 올바르면 한 번 인코딩
npx remotion render Captions out/demo.gif --codec=gif # README 증명 클립
npx remotion compositions를 사용하여 durationInFrames/fps를 읽고 현재 단어 및 끝 프레임을 선택하십시오.
완료 전 확인 사항:
- 스틸이 프레임 0, 중간 현재 단어 프레임, 마지막 프레임에서 깨끗하게 렌더링됨 — 누락된 글꼴/오디오 없음.
- 샘플링된 프레임에서 올바른 단어가 하이라이트됨 (frame/fps가 해당 토큰 내에 있음); 균등 분할 가짜 없음.
- 인코딩 자막이 가독성이 좋음 (테두리+그림자) 그리고 확인된 모든 프레임에서 자막이 9:16 안전 영역 내에 완전히 포함됨.
- 프레임 기반 전용 —
Date.now()/Math.random()/ 타이머 없음. - 제공된 프롭스 (실제 토큰,
defaultProps뿐만 아니라)가 올바르게 렌더링됨; MP4 + 보조 SRT/VTT 생성, GIF는 선택 사항.
참고 파일
references/word-timed-captions.md— 종단 간 구축: Whisper 전사 및Caption타입, 완전한 SRT 파서, 현재 단어 하이라이트가 있는 Remotion 단어별 컴포넌트, 수동 페이지 매김, SRT/VTT 생성기, 플랫폼별 안전 영역 맵, 가독성 좋은 타이포그래피 사양 시트.