Communitygithub.com

caption-animation

음성 대본이나 나레이션을 단어별로 정확하게 맞춘, 시선을 사로잡는 자막 애니메이션을 생성합니다.

caption-animation란 무엇인가요?

이 스킬은 사용자의 음성 대본(transcript)이나 나레이션을 입력받아, 각 단어가 음절에 맞춰 등장하고 현재 재생 중인 단어가 하이라이트되는 '단어별 시간 동기화(word-timed)' 애니메이션 자막을 제작하는 AI 어시스턴트의 능력을 정의합니다. 주로 TikTok, Reels, Shorts와 같은 숏폼 수직 영상(9:16)에 최적화되어 시청 시간을 높이는 효과적인 자막을 만듭니다. 이 스킬의 핵심은 Whisper나 AssemblyAI 같은 음성 인식 툴을 사용해 단어 수준의 타임스탬프를 추출하고, 이를 Remotion(프레임 기반 자막 애니메이션) 또는 순수 CSS/웹 기술을 이용해 시각적으로 표현하는 파이프라인을 제공한다는 점입니다. 특히 각 단어가 미세하게 튀어오르는(overshoot) 스프링 애니메이션과 현재 단어 하이라이트 기능을 통해 생동감을 주며, 1~4개 단어씩 끊어 보여주는 '페이지' 방식으로 긴 텍스트를 피합니다. 또한 가독성을 최우선으로 하여 굵은 산세리프 폰트, 2-6px 테두리(stroke), 그림자 효과를 적용하고, 자막이 플랫폼 UI(사용자 이름, 음악 버튼 등)와 겹치지 않도록 안전 영역(center band, 아래쪽 280px/15% 여백) 안에 배치하는 규칙을 따릅니다. 최종 결과물로는 소셜 미디어 업로드용 MP4(인코딩된 자막)와 접근성/SEO를 위한 SRT/VTT 파일을 함께 내보냅니다.

지원 대상~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/iart-ai/tiktok-video-skills/tree/main/skills/caption-animation

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

자막 애니메이션 (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 렌더 / 파일 생성

두 가지 불가침 규칙

  1. 단어 수준 타이밍, 줄 수준이 아님. 노래방 자막은 각 단어가 음절에 정확히 맞을 때만 마법처럼 느껴집니다. 항상 단어 타임스탬프를 추출하고, 줄 지속 시간을 균등하게 나누어 가짜로 만들지 마십시오. 불일치는 즉시 눈에 띕니다.
  2. 가독성이 스타일보다 우선합니다. 자막은 햇빛 아래, 음소거 상태의 휴대폰에서 읽힙니다. 굵은 산세리프, 두꺼운 테두리 또는 그림자, 높은 대비가 먼저입니다. 장식은 그 다음입니다. 아무도 읽을 수 없는 자막은 자막이 아니라 장식일 뿐입니다.

대본에서 단어 타이밍 얻기

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}&nbsp;</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를 읽고 현재 단어 및 끝 프레임을 선택하십시오.

완료 전 확인 사항:

  1. 스틸이 프레임 0, 중간 현재 단어 프레임, 마지막 프레임에서 깨끗하게 렌더링됨 — 누락된 글꼴/오디오 없음.
  2. 샘플링된 프레임에서 올바른 단어가 하이라이트됨 (frame/fps가 해당 토큰 내에 있음); 균등 분할 가짜 없음.
  3. 인코딩 자막이 가독성이 좋음 (테두리+그림자) 그리고 확인된 모든 프레임에서 자막이 9:16 안전 영역 내에 완전히 포함됨.
  4. 프레임 기반 전용 — Date.now() / Math.random() / 타이머 없음.
  5. 제공된 프롭스 (실제 토큰, defaultProps뿐만 아니라)가 올바르게 렌더링됨; MP4 + 보조 SRT/VTT 생성, GIF는 선택 사항.

참고 파일

  • references/word-timed-captions.md — 종단 간 구축: Whisper 전사 및 Caption 타입, 완전한 SRT 파서, 현재 단어 하이라이트가 있는 Remotion 단어별 컴포넌트, 수동 페이지 매김, SRT/VTT 생성기, 플랫폼별 안전 영역 맵, 가독성 좋은 타이포그래피 사양 시트.

Individual skills in this repo

This repo contains 3 individual skills — each has its own dedicated page.

관련 스킬