캡션 애니메이션
음성 해설이나 대본을 단어 단위로 동기화되는, 시선을 사로잡는 캡션으로 변환하세요. 각 단어가 음절에 맞춰 나타나고, 현재 재생 중인 단어가 강조되며, 텍스트는 내레이션에 정확히 맞춰집니다. TikTok, Reels, Shorts에서 시청 시간을 늘려주는, 읽기 쉽고 박자에 맞으며 안전 영역 안에 배치되는 캡션을 만드세요.
사용 시기
- 쇼트폼 세로 동영상(9:16)에서 단어별 또는 노래방 스타일 캡션
- 음성 해설/내레이션 트랙에 대본에서 추출한 오픈 캡션을 영구 삽입
- Whisper/SRT 대본을 Remotion 또는 웹에서 애니메이션 텍스트로 변환
- 자동 생성 자막을 브랜드화된 "호르모지 스타일" 공개 효과로 재스타일링
파이프라인
| 단계 | 작업 | 도구 |
|---|---|---|
| 전사 | 오디오 → 단어 수준 타임스탬프 | Whisper (@remotion/install-whisper-cpp), AssemblyAI |
| 정규화 | → { text, startMs, endMs }[] 토큰 | @remotion/captions Caption 타입 |
| 페이지 분할 | 단어를 1–4개 단어 "페이지"로 그룹화 | createTikTokStyleCaptions() |
| 애니메이션 | 단어별 팝인 + 활성 단어 강조 | Remotion spring() / CSS |
| 배치 | 안전 영역, 읽기 쉬운 타이포그래피 | 아래 레이아웃 규칙 참조 |
| 내보내기 | 영구 삽입(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>
);
};
frame을 각 토큰의 [startMs, endMs]에 대해 테스트하여 active를 계산하세요. 강조 표시는 단일 악센트 색상이나 현재 재생 중인 단어 뒤에 채워진 "박스"로 유지하세요. 한 번에 모든 단어의 색상을 애니메이션하지 마세요.
텍스트 벽이 아닌 페이지
한 번에 1–4개의 단어를 표시하세요. Remotion의 createTikTokStyleCaptions()를 사용하거나 수동으로 그룹화하고 combineTokensWithinMilliseconds를 조정하세요. 진정한 단어별 효과는 약 200500ms, 짧은 읽기 가능 구문은 약 10001500ms입니다.
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)
| 속성 | 값 | 이유 |
|---|---|---|
| 글꼴 | 굵은/아주 굵은 산세리프 (Montserrat, Inter, Helvetica) | 작고 복잡한 화면에서 읽기 용이 |
| 크기 | 56–80px (프레임 높이의 약 8%), 최소 45px | 휴대폰에서 음소거 상태로 읽을 수 있음 |
| 외곽선 | 2–6px 검은색 실선 외곽선 | 모든 배경에서 선명함 유지 |
| 그림자 | 외곽선 백업으로 부드러운 드롭 섀도우 | 밝은 프레임에서 구분 |
| 대소문자 | 대문자 또는 문장 형태, 높은 대비 채우기 | 임팩트 + 스캔 가능성 |
| 세로 위치 | 중앙 밴드, 아래에서 약 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)가 기본값입니다.
검증 루프 — 스틸 렌더링 → 검사 → 인코딩. 단어 타이밍이 가장 중요한 부분입니다. 인코딩에 시간을 소비하기 전에 정확한 프레임에서 확인하세요.
# 시작 / 샘플링된 활성 단어 프레임 / 끝의 스틸 — 제공된 PROPS 사용 (실제 토큰 + 오디오)
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()/ 타이머 없음. - 제공된 props (실제 토큰,
defaultProps뿐만 아니라)가 올바르게 렌더링되는지 확인; MP4 + 사이드카 SRT/VTT 생성, GIF는 선택 사항.
참조 파일
references/word-timed-captions.md— 종단간 구축: Whisper 전사 및Caption타입, 전체 SRT 파서, 활성 강조 표시가 있는 Remotion 단어 타이밍 컴포넌트, 수동 페이지 매김, SRT/VTT 생성기, 플랫폼별 안전 영역 맵 및 가독성 타이포그래피 사양 시트.