# 21 · 영상 도구 사용법

**언제 읽나**: 영상·3D·수학애니메이션·아트를 실제로 만들 때.
**전제**: 설치가 끝나 있어야 한다. 안 됐으면 `20-motion-install.md`.

---

## 먼저: 어느 도구를 쓸지 고른다

| 만들려는 것 | 쓸 도구 |
|---|---|
| 데이터가 바뀌면 영상도 바뀌어야 함 (차트·리포트·반복 생성) | **Remotion** |
| 브랜드 영상·타이틀·설명 영상 (웹 디자인 그대로) | **HyperFrames** |
| 수식 전개·그래프·알고리즘 설명 | **Manim** |
| 3D 장면·제품 렌더 | **Blender MCP** |
| 배경 패턴·추상 비주얼·플로터 출력 | **algorithmic-art** |
| 말을 글로 / 자막 | **whisper.cpp** |
| 앱·웹에 심는 애니메이션(MP4 아님) | **text-to-lottie** |
| 실사 푸티지 조립·나레이션 영상 | **OpenMontage** |
| 위가 섞임 | 구간별로 각자 뽑고 **ffmpeg로 합친다** |

❌ 한 도구로 전부 하려고 하지 마라. 가장 흔한 실패다.

---

## Remotion — React로 영상

핵심 3가지만 지킨다:
1. `useCurrentFrame()`으로 현재 프레임을 받아 **그 시점의 화면**을 그린다.
2. 값 변화는 `interpolate(frame, [입력], [출력])` 또는 `spring()`.
3. ❌ **CSS transition/animation을 쓰지 마라.** 렌더는 재생이 아니라 프레임 시킹이라 반영되지 않는다.

```tsx
import { AbsoluteFill, interpolate, useCurrentFrame } from "remotion";

export const Hello: React.FC = () => {
  const frame = useCurrentFrame();
  const opacity = interpolate(frame, [0, 20], [0, 1], { extrapolateRight: "clamp" });
  return (
    <AbsoluteFill style={{ background: "#0f172a", justifyContent: "center", alignItems: "center" }}>
      <div style={{ opacity, color: "#a3e635", fontSize: 84, fontWeight: 800 }}>Remotion OK</div>
    </AbsoluteFill>
  );
};
```

등록(Root.tsx)과 렌더:
```tsx
<Composition id="Hello" component={Hello}
  durationInFrames={90} fps={30} width={1280} height={720} />
```
```bash
npx remotion render Hello out/hello.mp4
# 데이터 주입:
npx remotion render Hello out/a.mp4 --props='{"title":"A"}'
```

---

## HyperFrames — HTML로 영상

반드시 지킬 3가지:
1. GSAP 타임라인을 **`paused: true`** 로 만든다.
2. 요소마다 `data-start`, `data-duration`으로 존재 구간을 선언한다.
3. 타임라인을 **`window.__timelines["<composition-id>"]`** 에 등록한다.

셋 중 하나라도 빠지면 영상이 정지 화면으로 나온다.

```html
<div id="root" data-composition-id="main"
     data-start="0" data-duration="6" data-width="1920" data-height="1080">
  <h1 id="title" class="clip" data-start="0" data-duration="6" data-track-index="0">Title</h1>
</div>
<script>
  const tl = gsap.timeline({ paused: true });
  tl.fromTo("#title", { opacity: 0, y: 28 },
                      { opacity: 1, y: 0, duration: 0.8, ease: "power3.out" }, 0);
  window.__timelines["main"] = tl;
  tl.seek(0);
</script>
```

```bash
npm run check    # 먼저 실행. 린트+모션+대비(WCAG). 렌더보다 훨씬 빠르다
npm run render   # mp4 생성
```

통과 예시 출력: `0 issues across 9 sample(s)` / `Motion 0 errors` / `Contrast 59/59 pass WCAG AA`

---

## Manim — 수식·도형

핵심 3가지:
1. `Scene.construct()`가 진입점.
2. `self.play(애니메이션)` 으로 재생. 애니메이션: `Write`, `Create`, `FadeIn`, `Transform`.
3. 기존 객체를 바꿀 땐 **`obj.animate.메서드()`**. ❌ `.animate`를 빼면 즉시 적용되어 움직임이 없다.

```python
from manim import *

class HelloManim(Scene):
    def construct(self):
        title  = Text("Motion Stack OK", font_size=56, color=GREEN)
        circle = Circle(radius=1.4, color=BLUE).shift(DOWN * 1.2)
        self.play(Write(title), run_time=1.2)
        self.play(title.animate.shift(UP * 1.6), run_time=0.6)
        self.play(Create(circle), run_time=1.0)
        self.wait(0.4)
```

```bash
export PATH="$HOME/Library/Python/3.11/bin:$PATH"   # manim 못 찾을 때
manim -ql --disable_caching scene.py HelloManim     # 작업 중: 480p15 (빠름)
manim -qh scene.py HelloManim                       # 최종: 1080p60
# 출력 경로: ./media/videos/scene/<품질>/HelloManim.mp4
```

한글을 쓰면 폰트를 명시한다: `Text("한글", font="Pretendard")`

---

## Blender MCP — 3D

**먼저 Blender에서 Start MCP Server를 눌렀는지 확인한다.** (파일 `20`의 4번)

요청은 단계로 나눈다. 한 번에 다 시키지 않는다.
```
1단계: "지름 2짜리 구를 원점에 만들고 금속 재질(roughness 0.2)을 입혀줘"
2단계: (결과 확인 후) "카메라를 45도 위에서 내려다보게 배치해줘"
3단계: "HDRI 환경광을 넣고 1920x1080으로 렌더해줘"
```

---

## algorithmic-art

요청에 **반드시 시드 고정을 포함**한다. 없으면 같은 결과를 다시 만들 수 없다.
```
"코드로 제너러티브 아트 만들어줘.
 - 플로우 필드, 1080x1080, 어두운 배경, 3색 이내
 - 시드를 고정하고 파일명에 시드 값을 넣어줘
 - 플로터 출력용 SVG도 함께"
```

---

## whisper.cpp — 자막 (완전 오프라인)

```bash
# 1) 16kHz 모노 WAV로 변환 (이 형식만 받는다)
ffmpeg -y -i input.mp4 -ar 16000 -ac 1 -c:a pcm_s16le audio.wav

# 2) 받아쓰기
whisper-cli -m ~/.cache/whisper-models/ggml-large-v3-turbo.bin \
  -l ko -f audio.wav -osrt          # -ovtt -otxt -oj 도 가능

# 3) 영상에 넣기 — 소프트 자막(빠름, 끌 수 있음)
ffmpeg -y -i input.mp4 -i audio.wav.srt -c copy -c:s mov_text output.mp4

# 3') 하드 자막(각인, 재인코딩)
ffmpeg -y -i input.mp4 -vf "subtitles=audio.wav.srt:force_style='FontName=Pretendard,FontSize=22,BorderStyle=3'" \
  -c:v libx264 -crf 18 -c:a copy output.mp4
```

❌ 한국어인데 `-l ko`를 빼지 마라. 영어로 가정해 결과가 엉뚱해진다.
❌ "자막 생성 완료"라고만 보고하지 마라. **SRT를 열어 첫 줄을 보고에 포함한다.**

## 로컬 나레이션 (Kokoro TTS)

로그인·API 키 없이 음성을 만든다. HyperFrames가 자동으로 감지해 쓴다.
확인: `hyperframes doctor` → `TTS (Kokoro) Kokoro deps installed`

**전 구간 로컬 조합**: 화면(HyperFrames/Remotion) → 나레이션(Kokoro) → 자막(whisper.cpp) → 합성(ffmpeg).

## OpenMontage — 실사 푸티지 조립

```bash
cd openmontage
make demo-list                                   # 사용 가능한 zero-key 데모
.venv/bin/python render_demo.py code-to-screen   # 데모 렌더

# 직접 만들 때는 저장소를 열고 자연어로 지시한다(프로젝트형 도구다).
# 파이프라인 12종: 애니메이션 설명영상, 다큐(실사 아카이브), 토킹헤드, 트레일러 등
```

❌ 이 도구는 AGPLv3다. 결과물을 배포·서비스화하기 전에 라이선스를 확인한다.

## text-to-lottie — 산출물이 영상이 아니다

Lottie JSON을 만든다. 웹/React Native/iOS/Android에 심어 재생하는 벡터 애니메이션이다.
검증은 함께 설치한 Skia Skottie 플레이어(`lottie-player/`, `npm run dev`)에서 한다.
❌ MP4가 필요하면 이 도구를 쓰지 마라.

---

## 여러 도구 결과를 합치기

```bash
# 1) 사양이 같은지 먼저 확인 (다르면 합쳐도 깨진다)
for f in intro.mp4 formula.mp4 chart.mp4; do
  echo -n "$f: "
  ffprobe -v error -select_streams v:0 \
    -show_entries stream=width,height,r_frame_rate,codec_name -of csv=p=0 "$f"
done

# 2) 합치기 (사양이 모두 같을 때만 -c copy 가능)
printf "file 'intro.mp4'\nfile 'formula.mp4'\nfile 'chart.mp4'\n" > list.txt
ffmpeg -f concat -safe 0 -i list.txt -c copy final.mp4

# 3) 사양이 다르면 통일 후 재인코딩
ffmpeg -i formula.mp4 -vf "scale=1920:1080,fps=30" -c:v libx264 -crf 18 fixed.mp4
```

---

## 확인 절차 (완료 전 반드시)

```bash
# 1. 사양과 길이
ffprobe -v error -select_streams v:0 \
  -show_entries stream=codec_name,width,height \
  -show_entries format=duration,size -of default=nw=1 out.mp4

# 2. 내용 확인 — 프레임을 뽑아 실제로 본다
ffmpeg -y -ss 2 -i out.mp4 -frames:v 1 check.png
```

❌ **"렌더 성공" 로그를 결과가 맞다는 증거로 쓰지 마라.** 빈 영상도 성공 로그를 남긴다.
용량이 비정상적으로 작으면(10초 1080p가 수십 KB) 빈 화면일 가능성이 높다.
