Communitygithub.com

Kreesten-hsh/video

이 스킬은 사용자가 '움직이는 캡션 추가', '틱톡 캡션', '노래방 캡션', '단어별 자막', '자동 자막', '현재 단어 강조', '비디오에 캡션 굽기', '보이스오버/내레이션에 캡션 동기화' 또는 'SRT/대본을 움직이는 텍스트로 변환'을 요청할 때 사용해야 합니다. Whisper의 단어 수준 타이밍, 단어별 팝/스케일 인, 활성 단어 강조, 9:16 안전 영역 배치, 가독성 좋은 글꼴, 그리고 굽기(인코딩) 대 사이드카 SRT/VTT를 다룹니다.

video란 무엇인가요?

이 스킬은 대본이나 보이스오버를 단어별 타이밍이 정확한 움직이는 캡션으로 변환하는 Caption Animation 스킬입니다. 사용자가 TikTok, Reels, Shorts와 같은 숏폼 수직 비디오(9:16)에 단어별 또는 노래방 스타일의 캡션을 추가하거나, Whisper/SRT 대본을 Remotion 또는 웹에서 애니메이션 텍스트로 변환하고, 자동 생성 자막을 브랜드화된 'Hormozi 스타일'로 재구성해야 할 때 사용됩니다. 파이프라인은 Whisper 또는 AssemblyAI를 통한 오디오-단어 수준 타임스탬프 변환, `@remotion/captions` 라이브러리를 사용한 토큰 정규화 및 페이지 그룹화, Remotion의 `spring()` 함수나 CSS 애니메이션을 사용한 단어별 팝인 및 활성 단어 강조, 안전 영역 배치 및 가독성 규칙 적용, 최종 MP4 굽기 또는 SRT/VTT 내보내기로 구성됩니다. 두 가지 핵심 규칙은 단어 수준 타이밍을 반드시 사용하고(문장 수준 분할 금지), 가독성이 스타일보다 우선한다는 것입니다. 캡션은 9:16 프레임(1080×1920)의 중앙 밴드(세로 약 62~70%)에 배치되며, 볼드체 산세리프 글꼴(56~80px), 2~6px 외곽선, 그림자를 사용하여 작은 화면에서도 선명하게 보이도록 합니다. Remotion과 순수 웹(CSS/DOM) 플랫폼을 모두 지원하며, TikTok/Reels 같은 소셜 플랫폼에는 픽셀 단위로 굽는 방식을 사용하고 접근성(SRT/VTT)용으로는 사이드카 파일을 함께 제공합니다.

지원 대상~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/Kreesten-hsh/video/tree/HEAD/.agents/skills/caption-animation

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

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

문서

캡션 애니메이션

음성 해설이나 대본을 단어 단위로 동기화되는, 시선을 사로잡는 캡션으로 변환하세요. 각 단어가 음절에 맞춰 나타나고, 현재 재생 중인 단어가 강조되며, 텍스트는 내레이션에 정확히 맞춰집니다. 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/VTTRemotion 렌더링 / 파일 생성

두 가지 필수 규칙

  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>
  );
};

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를 읽고 활성 단어 + 끝 프레임을 선택하세요.

완료하기 전에:

  1. 스틸이 프레임 0, 중간 활성 단어 프레임, 마지막 프레임에서 깔끔하게 렌더링되는지 확인 — 누락된 글꼴/오디오 없음.
  2. 샘플링된 프레임에서 올바른 단어가 강조 표시되는지 확인 (frame/fps가 해당 토큰 내에 속해야 함); 균등 분할 가짜 금지.
  3. 영구 삽입이 가독성이 좋고 (외곽선+그림자) 캡션이 모든 확인된 프레임에서 9:16 안전 영역 내에 완전히 있는지 확인.
  4. 프레임 구동 전용 — Date.now() / Math.random() / 타이머 없음.
  5. 제공된 props (실제 토큰, 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.

관련 스킬