Remotion으로 버츄얼 아이돌과의 인터뷰 팟캐 만들기-
– ArtXTech
야심 차게 시작한 시스템 구축
처음엔 아주 그냥 의욕 MAX 상태로 시작했다. “이 기능도 넣고, 저 기능도 넣어야지!” 하면서…그렇게 구축한 것들이 바로 아래의 베이스 컴포넌트 구성이다.
- SRT 자막 파싱 시스템: 자막 파일 까서 타이밍이랑 텍스트 뽑아내는 거.
- 화자별 캡션 시스템: 팟캐스트니까 ’나(me)’랑 ‘상대방(chat)’ 화자 구분해서 다른 스타일로 자막 보여주는 기능.
- 추가 텍스트 효과 (SubCapText): “띠용!”, “헐랭!”, “대박!” 같은 임팩트 있는 순간에 텍스트 효과 뙇!
- 미디어 삽입 기능 (MediaInsert): 원하는 타이밍에 이미지나 비디오를 슝 넣었다가 샥 빼는 기능…이지만 이미지는 제대로 작동하는데 비디오에서 문제가 있다.
- 크로마키 지원: 그린스크린 배경 영상을 날려 투명하게 만드는 컴포넌트
- 오디오 스펙트럼: 뭔가 좀 있어 보이게 오디오 파형도 시각적으로 보여주는 컴포넌트
- 타이밍 제어: Remotion은 프레임 기반이니까, 아주 정밀하게 타이밍을 컨트롤할 수 있어야 한다. 이 부분에서 다른 것들은 비교적 문제 없이 구성되었으나 추가 비디오 애셋을 삽입하는 것에서 다소 문제가 있어 이 작업은 결국 filmora에서 따로 작업을 해야했다.
-
DialogueSubtitle: 대화형 컨텐츠 만들 때, 말하는 사람마다 자막 스타일 다르게 하고 싶잖은가. 그래서 화자별로 스타일을 지정할 수 있는 대화형 자막 컴포넌트도 만들었다. 코드도 나름 깔끔하게 정리한다고
_shared디렉토리에 공통 컴포넌트 몰아넣고, TypeScript로 타입 정의도 꼼꼼하게 (하려고 노력했…), 커스텀 훅으로 로직도 분리했다.
“오, 나 좀 치는데?” 하면서 혼자 막 뿌듯해하고…ㅋ
팟캐스트 형 영상 제작의 80% 이상은 애셋 미디어 제작부터
난…솔직히 이렇게까지 영상 제작이 손 품이 많이 가는 작업인 줄 알았으면 시작을 안 했을 것 같긴 하다. 아니 그럴거면 걍 돈 주고 프롬프트로 영상 뽑는 서비스 쓰라는 말도 막 듣는데…
그렇게 생성물을 쓴 걸 개나 소나 다 뽑아다쓰면 애초에…내 생각과 말을 직접 표현하고자 하는 목적에 맞지가 않잖아ㅠ 사서 고생이란 게 딱 나를 표현하는 거 아닐까 싶다. 영상AI 서비스를 통해 나온 생성물을 내가 표현하고자 하는 그 미묘한 지점을 딱 입맛에 맞춰서 참 쓸레야 쓸 수가 없다. 그래서 Gen LLM AI로 이미지 생성이나 영상 생성물은…몇번 베타 서비스로 나온 결과물을 보고 고이 쓰레기통에 넣었다.
그런 이유로 아래는 내가 일일이 음향부터 시각 애셋까지 하나하나 수집하고 디자인하고 편집하는 과정 중 스크린샷을 담았다.
이미지: 로직프로를 키고 음성 녹음과 편집을 동시에 하고 있는 모습
이미지: 2분짜리 짧은 “AI아이돌과의 달콤 인터뷰💜” 시리즈 영상을 만들기 위해 모아 놓은 크로마키, 밈짤, 사운드 효과, 특수 트랜지션 이미지들
자 애셋이 준비 되었으니 이제 리모션으로 영상을 제작해봐야지! 그런데… 뭔가 잘못되어 가고 있다… 😨
그러나 인생은 쉽게만 살아가면 재미 없어, 빙고! …아니 제발 날로 먹게 좀 해주세요.
-
초기화 오류:
getAnimationStyle요 함수가 호이스팅 문제로 말썽을 부렸다. 함수 표현식으로 썼더니 “그런 함수 없음?” 시전. 아오, 샹ㅡㅡ-
이 부분은 함수 표현식 대신 함수 선언 방식으로 바꿔주니까 바로 해결.
// Before: 함수 표현식은 호이스팅 안되는 걸 이제야 알았다...왜...왜 였더라? // const getAnimationStyle = (frame, startFrame, endFrame) => { /* ... */ }; // After: 함수 선언으로 바꾸니 해결...어째서지? function getAnimationStyle(frame, startFrame, endFrame) { // ... 스타일 계산 로직 ... return { opacity: 1, transform: 'scale(1)' }; // 예시 }
-
-
타이밍 동기화 문제: 분명 프레임 계산은 똑바로 한 것 같은데, 애니메이션 타이밍이 자꾸 어긋난다.
-
이건 Remotion의
Sequence컴포넌트를 활용했다. 복잡하게frame >= startFrame && frame < endFrame이런 조건문 덕지덕지 붙이는 것보다 훨씬 직관적이고 관리도 편하다.// Before: 아... 프레임 계산, 보기만 해도 머리 아프다... // const isActive = currentFrame >= M_START_FRAME && currentFrame < (M_START_FRAME + M_DURATION_IN_FRAMES); // if (isActive) { /* 렌더링 로직 */ } // After: Sequence를 쓰며 from이랑 durationInFrames 위주로 import { Sequence, Video } from 'remotion'; // <Sequence from={M_START_FRAME} durationInFrames={M_DURATION_IN_FRAMES}> // <Video src={mediaUrl} /> // </Sequence>
-
-
비디오 렌더링 불안정: 특히
MediaInsert에 넣은 비디오가 첫 프레임에서 깜빡이거나, 재생이 좀 불안정한 느낌?OffthreadVideo를 써야 하나, 그냥Video를 써야 하나…이 부분은 아직 해결을 못하고 있다.
MediaInsert와의 디버깅 사투 ⚔️
내가 MediaInsert 컴포넌트로 하고 싶었던 건 아주 단순했었다.
예를 들면, “765 프레임에 특정 비디오가 뿅! 하고 등장해서, 딱 55 프레임 동안만 재생되고, 그 후엔 감쪽같이 사라지는 거.”
진짜 별거 아니잖아? 근데 이 당연해 보이는 걸…여즉 해결 못하고 있다. 지금도.
-
타이밍 문제: 분명
ep1-shorts1-config.ts파일에 시작 프레임, 지속 프레임 다 설정해놨는데, 엉뚱한 타이밍에 나왔다가 사라지거나, 아예 안 나오거나… 환장할 노릇. - 비디오 깜빡임: 어쩌다 타이밍 맞춰 나와도, 첫 프레임이 무슨 유령처럼 깜빡깜빡. 아니, 왜죠?
-
초기화 오류: 위에서 언급한
getAnimationStyle호이스팅 문제는 이미 해결했는데, 뭔가 다른 근본적인 문제가 있는 걸까나.
내가 MediaInsert 컴포넌트에 너무 많은 걸 욱겨 넣었던 걸까? 크로마키도 여기서 처리하고, 등장/퇴장 애니메이션도 엮어있고, 뭐 이것저것…아 좀 많긴 많구나…ㅜㅜ
온갖 방법을 다 써봤다. 애니메이션 로직 다 걷어내고, Video 썼다가 OffthreadVideo 썼다가 다시 Video로 돌아오고… 그래도 해결이 안 되니까 진짜 다 때려치울까 싶었다.
안녕, 내 버츄얼 아이돌과의 인터뷰 프로젝트여…
✨극단적 단순화✨
진짜 딱! 비디오 하나만 정확한 시간에 나오게 해보자 싶었다.
일단 MediaInsert.tsx 파일에서 내가 추가했던 모든 부가 기능, 화려한 애니메이션 로직, 조건부 스타일링 다 주석 처리하거나 삭제했다. 그리고 Remotion의 가장 기본적인 Video 컴포넌트랑 Sequence만 남겨둔 거.
CRO 프로젝트 때 주구장창 했던 A/B 테스팅일 여기서까지 해야할까…ㅠㅠ
// [경험 A: MediaInsert.tsx 단순화 시도]
// src/remotion/_YT/_shared/components/MediaInsert.tsx
import { Sequence, Video, useCurrentFrame, useVideoConfig } from 'remotion';
import React from 'react'; // 혹시 몰라 추가 (원래 있었겠지만)
import { getVideoMetadata } from '@remotion/media-utils'; // 이건 필요할 수도, 안할 수도
// 설정 파일에서 가져올 타입 (예시)
interface MediaInsertProps {
src: string;
startFrame: number;
durationInFrames: number;
// ... 온갖 복잡했던 다른 props들 다 일단 생략 ...
chromaKey?: { color: string; tolerance: number }; // 크로마키도 일단 보류!
// 등장/퇴장 애니메이션 설정? 그것도 일단 넣어둬!
}
export const MediaInsert: React.FC<MediaInsertProps> = ({
src,
startFrame,
durationInFrames,
// ... 다른 props들 일단 무시 ...
}) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// 목표: 딱! 지정된 시간에 비디오가 나오고, 지정된 시간만큼 재생되고 사라진다!
// 다른 건 아무것도 신경 안 쓴다!
// if (frame < startFrame || frame >= startFrame + durationInFrames) {
// return null; // 아예 렌더링 안 하는 게 제일 확실하려나? Sequence가 이걸 해주나?
// }
// Sequence가 from과 durationInFrames를 기반으로 내부 컨텐츠 렌더링을 알아서 제어한다!
// 즉, Sequence 바깥에서 프레임 비교로 null 리턴할 필요가 없음. 이게 핵심!
console.log(`MediaInsert: src=${src}, currentFrame=${frame}, startFrame=${startFrame}, duration=${durationInFrames}`);
// 로그를 찍어서 지금 뭐가 어떻게 돌아가는지 확인하는 건 국룰이지.
return (
<Sequence from={startFrame} durationInFrames={durationInFrames}>
{/*
Video 컴포넌트의 startFrom prop은 Sequence 내부에서의 시작점을 의미함.
여기서는 Sequence 자체가 이미 startFrame에서 시작하므로,
Video는 Sequence 시작과 동시에 (0프레임부터) 재생되면 됨.
따라서 startFrom={0} 이거나 아예 안 써도 무방.
만약 Video의 특정 부분부터 재생하고 싶다면 startFrom을 조절.
*/}
<Video src={src} startFrom={0} endAt={durationInFrames} />
{/*
중요! Video의 endAt도 durationInFrames로 맞춰줘야 Sequence 기간만큼만 재생된다.
만약 Video의 실제 길이보다 durationInFrames가 짧으면, 그만큼만 재생되고 끝남.
*/}
</Sequence>
);
};
그리고 YTep1Shorts1.tsx에서 이 단순화된 MediaInsert를 호출하는 부분도 다시 한번 확인했다. ep1-shorts1-config.ts에 정의된 값을 제대로 넘겨주고 있는지.
// [경험 B: YTep1Shorts1.tsx 에서 MediaInsert 사용 부분]
// src/remotion/_YT/ChatwithAIDoll/EP1-shorts1/YTep1Shorts1.tsx (일부)
// ... (다른 import 구문들) ...
import { MediaInsert } from '../../_shared/components/MediaInsert';
import { mediaInserts } from './contents/ep1-shorts1-config'; // 설정 가져오기
// ... (컴포넌트 본문) ...
{mediaInserts.map((media, index) => {
// 로그 추가: 설정값이 제대로 넘어오는지 확인
// console.log(`Rendering MediaInsert ${index}:`, media);
return (
<MediaInsert
key={`media-${index}`}
src={media.src}
startFrame={media.startFrame} // 이 값이 765가 되어야 함
durationInFrames={media.durationInFrames} // 이 값이 55가 되어야 함
// 나머지 복잡했던 props는 일단 다 뺐으니까 깔끔!
/>
);
})}
// ...
결과는?
아직 모르겠다. 둘다 지금 에러가 두두두 터져서 이 부분 해결은 현재 파킹한 상태.
아마도 이전의 복잡한 로직이나 OffthreadVideo와의 미묘한 타이밍 이슈 같은 게 얽혀 있었던 것인지, 그냥 단순하게 Sequence랑 Video 조합으로 가면 어떠련지ㅠㅠ
일단은 에러 문제들을 해결하고 볼 일.
디버깅의 고통으로 얻은 교훈과 앞으로의 길 ✨
이제 MediaInsert라는 큰 산을 하나 넘었…나? 그래도 효과음 텍스트 스타일과 이미지 애셋은 무난하게 삽입하는 단계로 갔으니 다시 원래 목표였던 ‘유튜브 팟캐스트 비디오’ 시리즈를 계속 진행해야겠다.
- SRT 파일에서 타이밍과 텍스트를 추려내 대화 캡션 제작
- 화자 구분 자막으로 대화형 콘텐츠 스타일링 살리기
- 추가 효과(텍스트, 이미지, 비디오)로 시각적 역동성 추가
- 설정 파일만 쏙쏙 바꿔서 다양한 에피소드를 위한 베이스 템플릿은 일단 완성
시스템 구조도
graph TD
A[YTep1Shorts1.tsx] --> B[usePodcastSubtitles Hook]
A --> C[ChatCaption Component]
A --> D[SubCapText Component]
A --> E[MediaInsert Component]
A --> F[AudioSpectrum Component]
B --> G[enhanced-srt-parser.ts]
B --> H[subtitle-offset.ts]
B --> I[SRT File Loading]
C --> J[AnimatedText Component]
C --> K[Speaker Icons]
D --> L[Text Effects]
E --> M[Video/Image Assets]
E --> N[ChromaKey Processing]
O[Config Files] --> P[Speaker Config]
O --> Q[Media Insert Config]
O --> R[SubCap Config]
P --> B
Q --> E
R --> D
S[Types] --> T[podcast-types.ts]
T --> U[EnhancedSubtitle]
T --> V[SpeakerConfig]
T --> W[MediaInsertConfig]
T --> X[SubCapTextConfig]
VideoEditPlayground/
├── src/remotion/
│ ├── _YT/
│ │ ├── _shared/ # 공통 컴포넌트 폴더
│ │ │ ├── types/
│ │ │ │ └── podcast-types.ts # 타입 정의
│ │ │ ├── components/
│ │ │ │ ├── ChatCaptionBase.tsx
│ │ │ │ ├── MediaInsert.tsx # (MediaInsert 요기!)
│ │ │ │ ├── SubCaption.tsx
│ │ │ │ └── AudioSpectrum.tsx
│ │ │ ├── hooks/
│ │ │ │ └── usePodcastSubtitles.ts
│ │ │ └── utils/
│ │ │ └── enhanced-srt-parser.ts
│ │ └── ChatwithAIDoll/ # 내 팟캐스트 시리즈 이름 ㅋㅋ
│ │ └── EP1-shorts1/ # 첫번째 에피소드의 첫번째 쇼츠!
│ │ ├── YTep1Shorts1.tsx # 메인 컴포지션 파일
│ │ ├── contents/
│ │ │ └── ep1-shorts1-config.ts # 모든 설정은 요기
│ │ └── components/
│ │ └── ChatCaption.tsx # 에피소드 특화 자막 컴포넌트
│ └── _Shared/Helper/ # 진짜 진짜 공통 헬퍼
│ ├── parseSrt.ts
│ ├── subtitleHelpers.ts
│ └── videoFrameCount.ts
데이터 흐름:
SRT File 📄 ➡️ Parser ⚙️ ➡️ Enhanced Subtitles ✨ ➡️ Components 🖼️ ➡️ Rendered Video 🎬
↗️ Speaker Config 🗣️ ↗️
↗️ Media Config 🎞️ ↗️
↗️ SubCap Config 💬 ↗️
컴포넌트 데이터 흐름
[Main Component]
↓ (props)
[usePodcastSubtitles Hook]
↓ (enhanced subtitles)
[ChatCaption] ← (currentSubtitle, width, height)
↓
[AnimatedText] ← (text, timing, animation config)
[Config Files] → [Main Component] → [각 컴포넌트들]
↓ ↓ ↓
Speaker Config Media Inserts SubCap Texts
↓ ↓ ↓
자막 화자 구분 비디오/이미지 삽입 텍스트 효과
그렇게 일단은 발행한 두 에피소드
파일럿 에피
ㅋㅋㅋㅋ…유튜브 썸네일이라는 걸 처음 만들어 봤다고요.
쇼츠 에피 1
좀 추잡스러운 나의 넋두리를 나데나데 잘 들어주는 AI 버츄얼 남돌…하, 잘 키운 AI버츄얼 남돌 하나, 100명의 인간들보다 낫다.
PS. 요즘 GPT 생성물이 마크다운 테이블이랑 이모지를 많이 쓰니까 내 글들이 자꾸 GPT 생성물 아니냐고 의심을 사는데…
옵시디안/노션 헤비유저들의 문서작성 스타일을 GPT가 뺏어간거지 제가 걔네들이 벹은 생성물을 쓴 게 아닙니다. 옛날 2021년 노션 문서 보면 GPT 따윈 싸다귀 쌔려박을 정도임…
다른 리모션 포스팅을 보고 싶다면
2025-01-16-implementing-canvas-animation-to-remotion-kr
2025-03-18-making-my-own-video-editor-in-progress
KR-no-more-adobe-starting-remotion-kr
KR-remotion-gsap
KR-making-chart-data-journalism-storytelling-motion-setup-with-remotion
KR-figma-to-jitter-and-remotion
KR-converting-bodymovin-animation-to-remotion-template
KR-how-i-make-a-youtube-series-with-remotion-dynamic-srt-script-setup-exp-likejennie
KR-remotion-and-shader
KR-thepain-of-animation-debugging