비디오 제작 스킬
코드로 만드는 애니메이션 모션 그래픽(키네틱 타입, 설명 영상, 런칭 비디오, 애니메이션 차트, 다양한 시각적 스타일의 로어 서드)에는
motion-graphics스킬을 사용하세요. 이 스킬은 영상, 사진, 오디오 및 ffmpeg 편집을 위한 것입니다.
ffmpeg(코어 엔진)과 Python(프레임/그래픽용 Pillow, 고급 편집용 MoviePy)을 사용하여 프로그래밍 방식으로 비디오를 제작합니다. 항상 먼저 계획하고, 그 다음 제작하고, 마지막으로 검증하세요.
1. 제작 전 계획
명령어를 작성하기 전에 다음 사항을 결정하세요.
| 결정 사항 | 기본값 | 참고 |
|---|---|---|
| 화면 비율 | 16:9 (1920x1080) | 릴스/쇼츠/틱톡은 9:16 (1080x1920), 피드용은 1:1 (1080x1080) |
| 프레임 속도 | 30fps | 영화 느낌은 24, 화면 녹화/게임 플레이는 60 |
| 길이 | 요청대로 | 릴스/쇼츠 ≤ 60~90초 |
| 형식 | MP4 (H.264 + AAC) | 어디서나 재생 가능; WebM은 요청 시에만; GIF는 짧은 루프에만 |
| 오디오 | 선택 사항 | 음악, 음성 해설, 또는 무음 |
짧은 쇼트 리스트를 작성하세요: 각 장면 → 지속 시간, 시각적 요소, 텍스트, 전환, 오디오.
2. 도구 체인 확인
which ffmpeg ffprobe || (apt-get update && apt-get install -y ffmpeg)
python3 -c "import PIL, moviepy" 2>/dev/null || pip install pillow moviepy --break-system-packages
fc-list | head # 텍스트에 사용할 수 있는 글꼴 확인
3. 표준 인코딩 설정
사용자가 다른 요청을 하지 않는 한 모든 최종 렌더링에 다음을 사용하세요.
-c:v libx264 -preset veryfast -crf 23 -pix_fmt yuv420p -movflags +faststart \
-c:a aac -b:a 192k
-pix_fmt yuv420p— 휴대폰, QuickTime 및 브라우저에서 재생하는 데 필요합니다.-movflags +faststart— 비디오가 완전히 다운로드되기 전에 재생을 시작할 수 있게 합니다.-crf 18= 거의 무손실(파일 크기 증가),-crf 28= 파일 크기 감소, 품질 저하.-preset veryfast는 렌더링 속도를 빠르게 유지합니다. 최고 품질이 요청될 때만slow를 사용하세요.- H.264의 경우 너비와 높이는 짝수여야 합니다.
3b. 빠르게 렌더링 — 사용자가 기다리고 있습니다
전체 렌더링은 가장 느린 단계이므로 요청당 하나의 전체 렌더링만 계획하세요.
- 인코딩 전에 미리보기. 몇 개의 프레임(가장 바쁜 순간, 첫 번째 및 마지막 프레임)을 최종 크기로 추출하거나 그리고 프레임 구성, 텍스트 크기 및 색상을 확인하세요. 사진 비디오의 경우:
scripts/slideshow.py OUT.mp4 photos… --preview는 1초 안에 연락 시트를 만듭니다. - 하나의 거대한 그래프가 아닌 장면 단위. 각 장면을 동일한 크기, fps 및 인코더 설정으로 자체 파일로 인코딩하고, concat 디먹서와
-c copy로 결합한 다음,-c:v copy로 사운드를 마지막에 추가하세요. 나중에 하나의 장면을 수정하는 것은 해당 장면만 다시 인코딩하는 것을 의미합니다. - 전환은 장면 파일 내에 있습니다.(가장자리에서 페이드 인/아웃). 두 개의 완성된 파일을 교차 페이드하면 두 번째 전체 인코딩이 강제됩니다.
- 정지 영상을 저렴하게 유지.
-loop 1(매 프레임마다 이미지를 다시 디코딩) 대신loop필터로 그림을 한 번 디코딩하고 반복하세요:ffmpeg -framerate 30 -i still.png -vf "format=yuv420p,loop=-1:1,trim=duration=4,setpts=PTS-STARTPTS" -c:v libx264 -preset veryfast -crf 20 -pix_fmt yuv420p still.mp4 - 실제 이름 아래에 절대 반쯤 완성된 파일을 두지 마세요.
name.part.mp4로 작성하고 ffmpeg가 0으로 종료될 때만 이름을 바꾸세요. 중단된 렌더링은 재생될 수 있지만 짧게 잘립니다. - 다른 요청이 없으면 출력 크기와 30fps로 프레임을 그리세요. 웹 페이지를 프레임 단위로 캡처할 때는 PNG보다 JPEG(품질 ~90)로 저장하세요. 쓰기 및 읽기가 훨씬 빠릅니다.
10장의 사진 → 1080p로 측정: 기존의 단일 -loop 1 + xfade 그래프는 13.2초가 걸렸습니다. slideshow.py --crossfade는 6.4초, 장면 파일 경로는 8.9초(음악 포함)가 걸렸습니다.
완성된 비디오 변경:
| 변경 사항 | 방법 |
|---|---|
| 한 장면의 내용 변경 | 해당 장면 파일을 다시 렌더링하고 -c copy로 다시 결합 |
| 새 음악 / 엔딩 트리밍 | -c:v copy (새 음악: -map 0:v -map 1:a) |
| 캡션 굽기, 작은 복사본 만들기 | 완성된 파일을 한 번 다시 인코딩 |
| 모든 프레임에서 변경되는 사항 | 전체 렌더링은 피할 수 없음 |
4. 레시피
4.0 한 줄 명령어로 사진 슬라이드쇼
python3 scripts/slideshow.py out.mp4 photos/*.jpg --seconds 3 --fade 0.5 --music song.mp3 # 빠름, 검은색으로 페이드
python3 scripts/slideshow.py out.mp4 photos/*.jpg --crossfade # 실제 교차 페이드
python3 scripts/slideshow.py out.mp4 photos/*.jpg --zoom --size 1080x1920 # 켄 번스 효과, 세로 모드
4.1 이미지로 슬라이드쇼 만들기 (페이드 전환 포함), 수동으로
# 각 이미지를 3초 동안 표시, 0.5초 교차 페이드, 1080p로 크기 조정/패딩
# 각 정지 이미지는 한 번 디코딩되고 loop 필터로 반복됩니다(-loop 1보다 2배 빠름)
F="scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,setsar=1,format=yuv420p,loop=-1:1,trim=duration=3,setpts=PTS-STARTPTS,fps=30"
ffmpeg -framerate 30 -i img1.jpg -framerate 30 -i img2.jpg -framerate 30 -i img3.jpg \
-filter_complex "\
[0]$F[a];[1]$F[b];[2]$F[c];\
[a][b]xfade=transition=fade:duration=0.5:offset=2.5[ab];\
[ab][c]xfade=transition=fade:duration=0.5:offset=5[out]" \
-map "[out]" -c:v libx264 -preset veryfast -crf 23 -pix_fmt yuv420p -movflags +faststart slideshow.mp4
클립 n의 오프셋 = (이전 지속 시간의 합) − (n × 페이드 지속 시간).
4.2 정지 이미지에 켄 번스 효과 (느린 줌)
# d = 출력 프레임 수 (30fps에서 150 = 5초); -loop 불필요, zoompan이 프레임을 생성합니다
ffmpeg -i photo.jpg -vf "scale=3840:-2,zoompan=z='min(zoom+0.0008,1.2)':d=150:x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':s=1920x1080:fps=30" \
-c:v libx264 -preset veryfast -crf 23 -pix_fmt yuv420p zoom.mp4
먼저 3840으로 업스케일링하면 작은 이미지에서 zoompan이 보여주는 떨리는 움직임을 방지할 수 있습니다.
4.3 텍스트 오버레이 / 타이틀 카드
ffmpeg -i input.mp4 -vf "drawtext=fontfile=/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf:\
text='My Title':fontsize=72:fontcolor=white:box=1:[email protected]:boxborderw=20:\
x=(w-text_w)/2:y=h-150:enable='between(t,0,4)'" \
-c:v libx264 -preset veryfast -crf 23 -pix_fmt yuv420p -c:a copy titled.mp4
아포스트로피, 이모지 또는 라틴어가 아닌 스크립트(힌디어 등)가 포함된 텍스트의 경우, 해당 스크립트를 지원하는 글꼴을 사용하여 Pillow로 텍스트를 PNG로 렌더링한 다음 오버레이하세요:
ffmpeg -i input.mp4 -i title.png -filter_complex "overlay=(W-w)/2:H-h-100:enable='between(t,0,4)'" ...
4.4 빈 화면에서 단색 타이틀 카드 만들기
ffmpeg -f lavfi -i color=c=0x111827:s=1920x1080:d=3:r=30 \
-vf "drawtext=fontfile=/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf:text='Chapter 1':fontsize=96:fontcolor=white:x=(w-text_w)/2:y=(h-text_h)/2" \
-c:v libx264 -preset veryfast -crf 23 -pix_fmt yuv420p card.mp4
4.5 클립 자르기
# 정확한 자르기(재인코딩): 00:00:05부터 10초 동안
ffmpeg -ss 00:00:05 -i input.mp4 -t 10 -c:v libx264 -preset veryfast -crf 23 -pix_fmt yuv420p -c:a aac trimmed.mp4
# 빠른 자르기(재인코딩 없음, 가장 가까운 키프레임에서 자름)
ffmpeg -ss 00:00:05 -i input.mp4 -t 10 -c copy trimmed_fast.mp4
4.6 클립 결합
동일한 코덱/해상도/fps → concat 디먹서 (재인코딩 없음):
printf "file 'a.mp4'\nfile 'b.mp4'\nfile 'c.mp4'\n" > list.txt
ffmpeg -f concat -safe 0 -i list.txt -c copy joined.mp4
다른 소스 → 먼저 정규화(크기 조정, fps, 오디오 레이트)하거나 concat 필터 사용:
ffmpeg -i a.mp4 -i b.mp4 -filter_complex \
"[0:v]scale=1920:1080,setsar=1,fps=30[v0];[1:v]scale=1920:1080,setsar=1,fps=30[v1];\
[v0][0:a][v1][1:a]concat=n=2:v=1:a=1[v][a]" \
-map "[v]" -map "[a]" -c:v libx264 -preset veryfast -crf 23 -pix_fmt yuv420p -c:a aac joined.mp4
(클립에 오디오 트랙이 없으면 무음을 추가하세요: -f lavfi -i anullsrc=r=48000:cl=stereo 그리고 -shortest 사용.)
4.7 배경 음악 추가
# 비디오 아래에 음악, 끝에서 페이드 아웃, 비디오가 끝나면 중지
# st = 비디오 길이 - 페이드 길이 (여기서는 30초 비디오, 3초 페이드) — ffprobe로 길이를 얻습니다
ffmpeg -i video.mp4 -i music.mp3 -filter_complex \
"[1:a]volume=0.3,afade=t=out:st=27:d=3[m]" \
-map 0:v -map "[m]" -c:v copy -c:a aac -shortest with_music.mp4
# 기존 음성 아래에 음악 믹싱
ffmpeg -i video.mp4 -i music.mp3 -filter_complex \
"[1:a]volume=0.15[m];[0:a][m]amix=inputs=2:duration=first[a]" \
-map 0:v -map "[a]" -c:v copy -c:a aac mixed.mp4
4.8 자막 / 캡션
.srt 파일을 작성하세요:
1
00:00:00,000 --> 00:00:03,000
비디오에 오신 것을 환영합니다.
2
00:00:03,000 --> 00:00:06,500
이것이 우리가 다룰 내용입니다.
자막을 굽습니다:
ffmpeg -i input.mp4 -vf "subtitles=subs.srt:force_style='FontSize=24,PrimaryColour=&HFFFFFF,OutlineColour=&H000000,BorderStyle=1,Outline=2'" \
-c:v libx264 -preset veryfast -crf 23 -pix_fmt yuv420p -c:a copy captioned.mp4
4.9 소셜 미디어용 크기 조정 (세로 9:16)
# 검은색 막대 대신 흐린 배경 채우기
ffmpeg -i landscape.mp4 -filter_complex \
"[0]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,boxblur=20[bg];\
[0]scale=1080:-2[fg];[bg][fg]overlay=(W-w)/2:(H-h)/2" \
-c:v libx264 -preset veryfast -crf 23 -pix_fmt yuv420p -c:a copy vertical.mp4
4.10 속도 높이기 / 낮추기
# 2배 속도 (비디오 + 오디오)
ffmpeg -i in.mp4 -filter_complex "[0:v]setpts=0.5*PTS[v];[0:a]atempo=2.0[a]" -map "[v]" -map "[a]" fast.mp4
4.11 비디오를 GIF로 변환 (고품질)
ffmpeg -i in.mp4 -vf "fps=15,scale=640:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" -loop 0 out.gif
4.12 코드로 프레임 렌더링 (애니메이션, 차트, 모션 그래픽)
ffmpeg 필터로 충분하지 않을 때는 Python으로 각 프레임을 그리고 이어 붙이세요:
from PIL import Image, ImageDraw, ImageFont
import os, subprocess
W, H, FPS, SECONDS = 1920, 1080, 30, 4
os.makedirs("frames", exist_ok=True)
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 96)
for i in range(FPS * SECONDS):
t = i / (FPS * SECONDS)
img = Image.new("RGB", (W, H), (17, 24, 39))
d = ImageDraw.Draw(img)
x = int(-600 + t * (W + 600)) # 텍스트가 화면을 가로질러 움직입니다
d.text((x, H // 2 - 48), "Hello!", font=font, fill=(255, 255, 255))
img.save(f"frames/{i:05d}.png")
subprocess.run(["ffmpeg", "-y", "-framerate", str(FPS), "-i", "frames/%05d.png",
"-c:v", "libx264", "-preset", "veryfast", "-crf", "23",
"-pix_fmt", "yuv420p", "-movflags", "+faststart", "animation.mp4"], check=True)
더 부드러운 움직임을 위해 이징(예: t*t*(3-2*t))을 사용하세요.
4.13 MoviePy (빠른 고급 편집)
from moviepy import VideoFileClip, TextClip, CompositeVideoClip, concatenate_videoclips, AudioFileClip
clip1 = VideoFileClip("a.mp4").subclipped(0, 5)
clip2 = VideoFileClip("b.mp4").subclipped(2, 8)
video = concatenate_videoclips([clip1, clip2], method="compose")
music = AudioFileClip("music.mp3")
video = video.with_audio(music.subclipped(0, min(video.duration, music.duration))) # 음악이 더 짧을 수 있음
video.write_videofile("out.mp4", fps=30, codec="libx264", audio_codec="aac", preset="veryfast")
(MoviePy 2.x API가 표시되었습니다. 1.x는 subclip/set_audio를 사용합니다. pip show moviepy로 확인하세요.)
5. 전달 전 확인
확인 없이 비디오를 절대 전달하지 마세요:
# 지속 시간, 해상도, 코덱, fps
ffprobe -v error -show_entries format=duration:stream=codec_name,width,height,r_frame_rate -of default=nw=1 out.mp4
# 주요 순간에 정지 프레임을 추출하여 확인
ffmpeg -ss 1 -i out.mp4 -frames:v 1 check_1s.png
ffmpeg -i out.mp4 -vf "fps=1/3,scale=480:-1,tile=4x2" -frames:v 1 contact_sheet.png
체크리스트:
- 지속 시간이 계획과 일치
- 해상도와 화면 비율이 올바르며, 원치 않는 검은 막대나 늘어짐이 없음
- 텍스트를 읽을 수 있고 안전 여백 내에 있음(가장자리에서 약 5% 유지, 9:16의 경우 앱 UI가 있는 하단 약 15%는 피함)
- 전환이 의도한 위치에 적용됨
- 오디오가 있음(예상되는 경우), 클리핑되지 않고 깔끔하게 끝남
- 휴대폰에서 재생 가능: H.264,
yuv420p, 짝수 치수
6. 일반적인 문제
| 문제 | 해결책 |
|---|---|
| iPhone/QuickTime에서 비디오가 재생되지 않음 | -pix_fmt yuv420p 추가 |
| "width not divisible by 2" | -1 대신 scale=1280:-2 사용 |
-c copy 자르기 후 시작 부분에 검은 프레임 | 재인코딩하거나 -ss를 -i 앞에 두고 재인코딩 |
| concat 후 오디오 동기화 불일치 | 결합 전에 fps와 오디오 샘플 레이트(-ar 48000) 정규화 |
| xfade 오류 "inputs differ" | 모든 입력의 크기, fps, setsar=1, 픽셀 형식이 동일해야 함 |
drawtext가 ' 또는 :에서 깨짐 | 이스케이프하거나 textfile= / Pillow로 렌더링된 PNG 오버레이 사용 |
| 영어가 아닌 텍스트가 네모로 표시됨 | 해당 스크립트를 지원하는 글꼴 사용(예: Noto Sans Devanagari) |
| 파일 크기가 너무 큼 | CRF를 높이고(26~28), fps 또는 해상도를 낮춤 |
| 렌더링이 너무 느림 | -preset veryfast 또는 ultrafast 사용, 초안 작성 중에는 해상도 낮춤 |
7. 워크플로 요약
- 계획 — 화면 비율, fps, 길이, 쇼트 리스트, 오디오.
- 에셋 수집 — 이미지, 클립, 음악, 글꼴;
ffprobe로 확인. - 빠른 초안 — 낮은 해상도 /
ultrafast프리셋으로 타이밍 확인. - 최종 제작 — 표준 인코딩 설정(섹션 3).
- 검증 — ffprobe + 접촉 시트 + 체크리스트(섹션 5).
- 전달 — 최종 파일과 함께 한 줄 요약(지속 시간, 해상도, 포함 내용) 전송.