Back to creative
creative 5.3 min read 258 lines

manim-video-with-narration

Manim 애니메이션 영상 제작 + Qwen3-TTS 나레이션 자동 추가 — 텍스트/URL에서 대본 추출 → 장면별 나레이션 생성 → 비디오 속도 조정 → 음성 합성 최종 영상

Manim Video + Edge-TTS Narration Pipeline

Manim으로 애니메이션 영상을 제작하고, edge-tts로 한국어 나레이션을 생성하여 합성하는 전체 파이프라인입니다.

필드 규칙


  • 일반 영상은 항상 5분 이상으로 제작 (숏츠는 60초 이하)
  • 해상도: -qm (720p30) 기본, 프로덕션은 -qh (1080p60)
  • TTS: edge-tts (ko-KR-SunHiNeural) 사용. Qwen3-TTS/ACE-Step BGM 사용 불가
  • 속도 조정: 0.5x~1.2x 범위 내 유지. 벗어나면 대본 분할
  • 대본: 장면당 150자 이하 권장

전체 흐름

대본 준비 → 나레이션 대본 작성 → Manim 영상 렌더링
→ Qwen3-TTS 나레이션 생성 → 오디오/비디오 동기화 → 최종 합성

전제 조건

| 도구 | 용도 | 확인 명령 |
|------|------|-----------|
| Manim CE v0.19+ | 애니메이션 렌더링 | manim --version |
| ffmpeg | 영상/음성 처리 | ffmpeg -version |
| kaggle CLI | TTS 원격 실행 | kaggle --version |
| ~/.kaggle/kaggle.json | Kaggle API 인증 | ls ~/.kaggle/kaggle.json |
| 한국어 폰트 | Manim 텍스트 렌더링 | fc-list \| grep "Apple SD Gothic" |

한글 폰트 설정 (macOS)

KOR_FONT = "Apple SD Gothic Neo"  # macOS 기본 한글 폰트
MONO = "Menlo" # 영문/코드용 모노스페이스

파이프라인 상세

Step 1: 대본 준비

영상 대본 출처:

  • 사용자가 직접 제공한 텍스트
  • YouTube URL → youtube-content 스킬로 자막 추출
  • 기사/문서 → 요약 후 대본화

Step 2: 나레이션 대본 작성

장면별로 나레이션 텍스트를 작성합니다. 중요 규칙:

  • 장면 길이에 맞춰 작성 — 각 장면의 예상 길이를 고려하여 텍스트 길이 조절
  • 장면당 150자 이하 권장 — TTS 생성 시간이 길어지면 비디오와 심하게 불일치
  • 구어체 사용 — "합니다"보다 "해요", "입니다"보다 "이에요" 등 자연스러운 말투
  • 영어 고유명사는 한글로 표기 — "Claude Code" → "클로드코드", "Office Hours" → "오피스아워즈"
  • 숫자도 한글로 — "15~20분" → "십오에서 이십분" (TTS에서 더 자연스러움)
  • 문장은 짧게 — 한 문장에 핵심 메시지 하나씩

narrations = {
"Scene00_Title": "클로드코드 플러그인을 전부 지웠습니다. 그리고 단 두 개만 남겼더니, 지금이 제일 잘 됩니다.",
"Scene01_Problem": "클로드코드를 쓰다 보면 자주 겪는 문제가 있습니다. 결과물을 보면, 이게 아닌데 하는 상황이죠.",
# ... 각 장면별 대본
}

Step 3: Manim 영상 렌더링

manim-video 스킬을 참조하여 애니메이션을 제작합니다.

# 1. 드래프트 렌더링 (빠른 확인용)
manim -ql --disable_caching script.py Scene00 Scene01 Scene02 ...

2. 미디엄 품질 (텍스트 확인용)


manim -qm --disable_caching script.py Scene00 Scene01 Scene02 ...

한글 렌더링 시 주의사항:

  • "" (한글 인용부호) 사용 금지 → 파이썬 문자열 " 와 충돌
  • font=KOR_FONT 반드시 지정
  • font_size 최소 24 이상 (한글은 영문보다 크기 필요)

Step 4: Qwen3-TTS 나레이션 생성

qwen3-tts 스킬의 Kaggle 원격 실행 방식을 사용합니다.

4-1. 노트북 준비

장면별 나레이션을 한 번에 생성하는 노트북을 작성합니다:

# Cell 1: 설치
!pip install torch==2.6.0 torchvision torchaudio --index-url https://download.pytorch.org/whl/cu124 --quiet
!pip install qwen-tts scipy --quiet
!apt-get install -y ffmpeg > /dev/null 2>&1

Cell 2: 장면별 나레이션 생성


import os, subprocess, torch, gc, json
import scipy.io.wavfile as wavfile
import numpy as np
from qwen_tts import Qwen3TTSModel

tts = Qwen3TTSModel.from_pretrained(
"Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice",
device_map="cuda:0",
dtype=torch.float16,
)

narrations = {
"Scene00_Title": "...",
"Scene01_Problem": "...",
# ...
}

durations = {}
for scene_name, text in narrations.items():
wavs, sr = tts.generate_custom_voice(text=text, language="Korean", speaker="Sohee")
audio_np = np.array(wavs[0], dtype=np.float32)

wav_path = f"/kaggle/working/{scene_name}.wav"
mp3_path = f"/kaggle/working/{scene_name}.mp3"

wavfile.write(wav_path, sr, audio_np)
subprocess.run(["ffmpeg", "-y", "-i", wav_path, "-codec:a", "libmp3lame", "-q:a", "2", mp3_path],
capture_output=True)

result = subprocess.run(["ffprobe", "-v", "error", "-show_entries", "format=duration",
"-of", "default=noprint_wrappers=1:nokey=1", mp3_path],
capture_output=True, text=True)
durations[scene_name] = float(result.stdout.strip())

os.remove(wav_path)
gc.collect()
torch.cuda.empty_cache()

with open("/kaggle/working/durations.json", "w") as f:
json.dump(durations, f, indent=2)

4-2. 실행 및 다운로드

# 푸시
cd /tmp/kaggle-tts && kaggle kernels push -p .

상태 대기 (약 3~8분)


while true; do
status=$(kaggle kernels status icbm3112k/qwen3-tts-test)
if echo "$status" | grep -q "COMPLETE"; then break; fi
if echo "$status" | grep -q "ERROR"; then echo "FAILED"; exit 1; fi
sleep 30
done

다운로드


rm -rf /tmp/kaggle-tts-output && mkdir -p /tmp/kaggle-tts-output
kaggle kernels output icbm3112k/qwen3-tts-test -p /tmp/kaggle-tts-output

Step 5: 오디오/비디오 동기화

각 장면의 비디오 길이와 오디오 길이를 비교하고, 비디오 속도를 조정합니다:

import json, subprocess

with open("/tmp/kaggle-tts-output/durations.json") as f:
audio_durs = json.load(f)

scenes = ["Scene00_Title", "Scene01_Problem", ...] # 장면 목록
base = "/path/to/media/videos/script/720p30" # 렌더링된 비디오 경로
adjusted_dir = "/path/to/adjusted"

for s in scenes:
audio_d = audio_durs[s]
video_d = float(subprocess.run(
["ffprobe", "-v", "error", "-show_entries", "format=duration",
"-of", "default=noprint_wrappers=1:nokey=1", f"{base}/{s}.mp4"],
capture_output=True, text=True
).stdout.strip())

speed = video_d / audio_d # < 1 = 느리게 (오디오가 김), > 1 = 빠르게

# 비디오 속도 조정 + 나레이션 오디오 합성
subprocess.run([
"ffmpeg", "-y",
"-i", f"{base}/{s}.mp4", # 원본 비디오
"-i", f"/tmp/kaggle-tts-output/{s}.mp3", # 나레이션
"-filter_complex", f"[0:v]setpts={1/speed}*PTS[v]",
"-map", "[v]", "-map", "1:a",
"-c:v", "libx264", "-preset", "fast", "-crf", "23",
"-c:a", "aac", "-b:a", "128k",
"-shortest", "-movflags", "+faststart",
f"{adjusted_dir}/{s}.mp4"
])

Step 6: 최종 합성

# concat 파일 생성
for s in Scene00_Title Scene01_Problem ...; do
echo "file '${adjusted_dir}/${s}.mp4'" >> concat_final.txt
done

병합


ffmpeg -y -f concat -safe 0 -i concat_final.txt -c copy final_with_narration.mp4

프로젝트 디렉토리 구조

project-name/
├── script.py # Manim 애니메이션 스크립트
├── plan.md # 대본/장면 계획
├── narration/ # 나레이션 대본 텍스트
│ ├── Scene00_Title.txt
│ └── ...
├── adjusted/ # 오디오 동기화된 장면별 비디오
├── final_with_narration.mp4 # 최종 산출물
└── media/ # Manim 자동 생성
└── videos/script/
├── 480p15/ # 드래프트
└── 720p30/ # 미디엄/프로덕션

품질 가이드

| 항목 | 권장값 | 비고 |
|------|--------|------|
| 렌더링 품질 | -qm (720p30) | 균형 잡힌 품질/속도 |
| 프로덕션 | -qh (1080p60) | 최종 배포용 |
| 오디오 비트레이트 | 128kbps AAC | 나레이션용 충분 |
| 비디오 CRF | 23 | 용량/품질 균형 |
| 장면당 나레이션 | 150자 이하 | 비디오-오디오 불일치 최소화 |
| 비디오 속도 조정 한계 | 0.5x ~ 1.2x | 이 범위 밖이면 대본 수정 필요 |

오디오-비디오 불일치 해결 전략

불일치가 심할 때(속도 조정이 0.5x 미만이거나 1.5x 이상):

  • 나레이션 대본 단축 — 가장 좋은 방법. 핵심만 남기기
  • 비디오 애니메이션 추가 — wait() 시간 늘리거나 장면 추가
  • 분할 — 하나의 장면을 두 개로 나누어 나레이션 분배

⚠️ 함정 (Pitfalls)

  • 한글 인용부호 "": 파이썬 문자열 구분자와 충돌. 반드시 제거하거나 \" 로 이스케이프
  • Manim def construct(self:: 괄호 빠진 문법 오류 자주 발생. 항상 self) 확인
  • Kaggle PyTorch 버전: torch==2.6.0+cu124 고정 필수 (T4 호환)
  • 동시 Kaggle 커널: GPU 커널 동시 1개만 실행 가능
  • 한글 폰트 누락: font 파라미터 미지정시 깨진 글자 출력
  • torchaudio.save() 금지: 반드시 scipy.io.wavfile 사용
  • ffmpeg -shortest: 오디오가 비디오보다 길 때 비디오 끝에서 자동 잘림. 속도 조정으로 해결
  • concat 시 코덱 일치: -c copy 사용하려면 모든 파일 동일 코덱/해상도 필요

Related Skills / 관련 스킬

ace-step-music

Kaggle T4 GPU에서 ACE-Step 1.5 터보로 가사 없는 인스트루멘탈 음악 생성 — 30초~60초 곡, 프롬프트 기반

creative v1.0.0

architecture-diagram

Generate dark-themed SVG diagrams of software systems and cloud infrastructure as standalone HTML files with inline SVG graphics. Semantic component colors (cyan=frontend, emerald=backend, violet=database, amber=cloud/AWS, rose=security, orange=message bus), JetBrains Mono font, grid background. Best suited for software architecture, cloud/VPC topology, microservice maps, service-mesh diagrams, database + API layer diagrams, security groups, message buses — anything that fits a tech-infra deck with a dark aesthetic. If a more specialized diagramming skill exists for the subject (scientific, educational, hand-drawn, animated, etc.), prefer that — otherwise this skill can also serve as a general-purpose SVG diagram fallback. Based on Cocoon AI's architecture-diagram-generator (MIT).

ascii-art

pyfiglet(571폰트), cowsay, boxes, toilet 등으로 ASCII 아트 생성. API 키 불필요.

ascii-video

ASCII 아트 비디오 프로덕션 파이프라인 — 비디오/오디오/이미지를 컬러 ASCII 캐릭터 비디오(MP4, GIF)로 변환