Manim 1080p60 기술 비디오 설명 & 동기화 음성 해설 시스템
개요 및 번들 리소스
pdflatex나 MacTeX 없이 Manim Community를 사용하여 순수 Python으로 선명하고 결정론적인 1920x1080 @ 60fps 기술 설명 비디오 및 아키텍처 워크스루를 제작합니다.
선택적으로 샘플 정확도의 신경망 음성 해설(오프라인 Piper ONNX, OpenAI 호환 /v1/audio/speech HTTP 엔드포인트, 또는 커스텀 CLI TTS), EBU R128 음량 정규화, 동기화된 .vtt WebVTT 자막, 그리고 CDN 없는 독립형 HTML5 챕터 플레이어와 애니메이션을 페어링합니다.
assets/nord_manim_theme.py: Nord Polar/Frost/Aurora 팔레트 토큰, 안전 프레임 레이아웃 상수,BeatTimer동기화 헬퍼, LaTeX-Free 프리미티브(make_header_banner,make_node_card,make_badge,animate_blocked_pulse,safe_fit_text), 그리고 엄격한 포트 앵커 기반 직교 도식 커넥터(connect_h,connect_v,connect_elbow,animate_packet_along,assert_all_orthogonal).assets/video_player_template.html: CDN 없는 독립형 Nord 다크/라이트 HTML5 시네마 플레이어. 챕터 탭, 클릭 가능한 비트 마커, 재생 속도(0.75x..2.0x), 선택적 이중 언어 오디오/자막 전환 지원.check_scene_bounds.py: 렌더링 전 레이아웃 검증 도구. Manim 장면 또는 JSON 체크포인트에서[-6.8, 6.8] x [-3.75, 3.75]프레임 오버플로 및 형제 텍스트 레이블 충돌을 확인합니다.mux_narration.py: 표준 라이브러리 기반 Python CLI. Piper, OpenAI 호환 HTTP/v1/audio/speech엔드포인트, 또는 커스텀 TTS 명령어를 통해 비트별 WAV를 합성하고,beat_timings.json을 생성하며, 샘플 정확도의 마스터 WAV 트랙을 조립하고, 음성이 잘리지 않도록 복제 패딩(tpad=stop_mode=clone)을 적용하며, 선택적으로 음량을 정규화(--loudnorm-lufs -18.0)하고, H.264 + AAC (+faststart)를 먹싱하며,.vtt자막을 작성합니다.lint.py: 출력 번들 검증 도구.ffprobe로 H.264yuv420p+ 오디오 스트림, WebVTT 헤더/큐 단조성, 오프라인 HTML5 플레이어 CSP/CDN-제로/이모지-제로 조건을 확인합니다.references/manim-layout-and-choreography.md: 다중 노드 토폴로지, 직교 버스/엘보우 라우팅, 동적 카운터, 양방향 화살표, 또는 다중 장면ffmpeg연결을 설계할 때 참조하세요.references/voiceover-and-commentary.md: 나레이션 매니페스트 작성, 자연스러운 나레이터 운율 지침, 발음 용어집, EBU R128 정규화, 또는 이중 언어 음성 해설 트랙을 작성할 때 참조하세요.
1. 스토리보드 및 다중 액트 내러티브 설계
Manim 코드를 작성하기 전에 설명을 개별 액트와 비트별 스토리보드 테이블로 구조화하세요.
액트당 하나의 Scene 클래스를 유지하여 액트 2를 편집할 때 액트 1 또는 액트 3을 다시 렌더링할 필요가 없도록 합니다:
- 액트 1 — 설정 및 요청 승인: 토폴로지, 불변 조건, 진입 가드를 소개합니다 (예:
Client -> API Gateway -> Token-Bucket Rate Limiter). - 액트 2 — 정상 상태 실행: 기본 데이터 경로와 상태 전환을 추적합니다 (예:
Read-Through Cache단일-비행 조회 ->Primary DB수화 -> TTL 쓰기 저장). - 액트 3 — 실패 모드 및 복구: 제한이 트리거되거나 노드가 실패할 때 발생하는 상황을 보여줍니다 (예:
HTTP 429버스트 거절 또는Distributed Lock & Write-Ahead Log Replication쿼럼 승격).
코딩하기 전에 모든 비트를 명시적으로 매핑하세요:
beat_id | visual_action | min_visual_sec | commentary_script (내레이션 시) |
|---|---|---|---|
act1_ingress | 4개의 Nord 토폴로지 카드 + 헤더 렌더링; Gateway로 요청 패킷 애니메이션 | 3.2 | 인바운드 쓰기는 상태 저장소에 닿기 전에 엣지 게이트웨이를 통과합니다. |
act1_bucket | 토큰 카운터 100 -> 99 감소; 녹색 ADMITTED 배지 깜빡임 | 3.8 | 원자적 토큰 버킷 검사는 다운스트림 동시성을 제한하면서 트래픽 버스트를 흡수합니다. |
act1_reject | 토큰이 0이 되었을 때 animate_blocked_pulse (429 RATE LIMITED) 트리거 | 3.5 | 버스트 용량이 소진되면 초과 호출자는 무한 대기 대신 HTTP 429로 빠르게 실패합니다. |
2. LaTeX-Free Manim Community 저작
격리된 환경 설정
격리된 가상 환경 내에서 Manim (및 선택적 piper-tts)을 설치하고 ffmpeg와 ffprobe가 PATH에 있는지 확인하세요:
uv venv venv
uv pip install --python venv/bin/python manim piper-tts
엄격한 LaTeX-Free 및 직교 도식 규칙
- Zero TeX 하위 프로세스:
Tex,MathTex,Title또는DecimalNumber(pdflatex/dvisvgm호출)를 절대 사용하지 마세요.Text,MarkupText(<b>,<i>,<sub>,<sup>,<span foreground="...">),Code,RoundedRectangle,Arrow,DashedVMobject,MoveAlongPath를 사용하세요. - 황금 도식 규칙 (포트-앵커 직교 와이어): 아키텍처 블록 다이어그램을 구축할 때 두
Mobject에 대해Arrow(card_a, card_b)를 절대 직접 호출하지 마세요 — Manim은 경계 상자 중심 사이로 광선을 조준하여 카드 너비나 Y 위치가 다를 때마다 비뚤어진 대각선 화살표를 생성합니다. 모든 엣지는connect_h(card_from, card_to, y=...),connect_v(card_from, card_to, x=...)또는connect_elbow(points)를 통해 라우팅하고,animate_packet_along(self, edge, ...)로 패킷을 애니메이션하며,assert_all_orthogonal(connectors, cards)로 와이어 지오메트리를 검증하세요. - 안전-프레임 좌표 그리드: Manim의 16:9 캔버스는
[-7.11, 7.11] x [-4.0, 4.0]범위입니다. 모든 작성 콘텐츠는[-6.6, 6.6] x [-3.5, 3.5]내에 유지하고, WebVTT 자막이 표시될 때마다y < -3.05는 비워두세요. assets/nord_manim_theme.py임포트:self.camera.background_color = BG_DARK로 설정하고 카드, 직교 커넥터, 패킷 애니메이션, 비트 홀드를 일관되게 구성하세요:
from pathlib import Path
import sys
SKILL_DIR = Path(__file__).resolve().parent
sys.path.insert(0, str(SKILL_DIR))
sys.path.insert(0, str(SKILL_DIR / "assets"))
from manim import Create, FadeIn, Scene, VGroup
from check_scene_bounds import dump_checkpoints
from nord_manim_theme import (
ACCENT_BLUE, ACCENT_CYAN, BG_DARK, STATUS_ERR, STATUS_OK,
BeatTimer, animate_blocked_pulse, animate_packet_along,
assert_all_orthogonal, connect_h, make_header_banner, make_node_card,
)
class Act1RateLimiter(Scene):
def construct(self) -> None:
self.camera.background_color = BG_DARK
timer = BeatTimer("tmp/beat_timings.json")
checkpoints: list[dict] = []
header = make_header_banner(
"Act 1 / Request Admission",
"API Gateway & Token-Bucket Rate Limiter",
)
gw = make_node_card("API Gateway", "TLS + Route Match", width=2.5, accent=ACCENT_CYAN).move_to([-4.5, 0.2, 0])
limiter = make_node_card("Rate Limiter", "Bucket: 100 tok", width=2.5, accent=ACCENT_BLUE).move_to([-1.5, 0.2, 0])
cache = make_node_card("Read Cache", "Single-Flight TTL", width=2.5, accent=ACCENT_CYAN).move_to([1.5, 0.2, 0])
db = make_node_card("Primary DB", "WAL Quorum", width=2.5, accent=STATUS_OK).move_to([4.5, 0.2, 0])
e_gw_lim = connect_h(gw, limiter)
e_lim_cache = connect_h(limiter, cache)
e_cache_db = connect_h(cache, db, dashed=True)
assert_all_orthogonal([e_gw_lim, e_lim_cache, e_cache_db], [gw, limiter, cache, db])
edges = VGroup(e_gw_lim, e_lim_cache, e_cache_db)
self.play(FadeIn(header), FadeIn(gw), FadeIn(limiter), FadeIn(cache), FadeIn(db), Create(edges), run_time=1.1)
spent = animate_packet_along(self, e_gw_lim, label="POST /orders", run_time=0.7)
timer.hold(self, "act1_ingress", consumed_anim_s=1.1 + spent, fallback_s=3.2, checkpoints=checkpoints)
spent = animate_packet_along(self, e_lim_cache, label="token=99", color=STATUS_OK, run_time=0.7)
timer.hold(self, "act1_bucket", consumed_anim_s=spent, fallback_s=3.8, checkpoints=checkpoints)
spent = animate_blocked_pulse(self, limiter, reason_label="429 RATE LIMITED", color=STATUS_ERR, run_time=0.95)
timer.hold(self, "act1_reject", consumed_anim_s=spent, fallback_s=3.5, checkpoints=checkpoints)
dump_checkpoints(checkpoints, "tmp/act1_bounds.json")
1080p60 렌더링 전에 경계 및 충돌 없음 확인
전체 1080p60 렌더링을 시작하기 전에 항상 몇 초 만에 레이아웃 지오메트리를 확인하세요:
# 옵션 A: 모든 Scene.wait() 호출에 대한 라이브 드라이-런 후크 (< 3초, MP4 인코딩 없음)
./venv/bin/python .agents/skills/manim-video-explainer/check_scene_bounds.py \
--module scenes.py --scene Act1RateLimiter
# 옵션 B: 빠른 -ql 렌더링 후 방출된 체크포인트 JSON 검사
python3 .agents/skills/manim-video-explainer/check_scene_bounds.py \
--json tmp/act1_bounds.json
다중 계층 좌표 템플릿, 동적 ValueTracker 카운터, 충돌 방지 정리 규칙은 references/manim-layout-and-choreography.md를 참조하세요.
3. 선택적 비트 동기화 음성 해설 코멘터리 (mux_narration.py)
황금 내레이션 규칙
화면의 상자 제목, 화살표, 배지 문자열을 그대로 읽지 마세요.
시청자는 이미 "API Gateway -> Rate Limiter"를 즉시 봅니다. 각 전환이 왜 발생하는지, 어떤 불변 조건이 시스템을 보호하는지, 거절 또는 복구의 원인이 무엇인지 설명하는 설명적인 프로세스 코멘터리를 작성하세요.
비트-퍼스트 투-패스 워크플로우
narration_manifest.json작성: 깔끔한 자막text와 발음 오버라이드(tts_phonetic_text또는phonetic_glossary)를 분리하여 기술 약어(WAL,gRPC,TTL,p99,HTTP 429)가.vtt자막을 오염시키지 않고 자연스럽게 발음되도록 합니다:
{
"lead_in_s": 0.20,
"inter_beat_gap_s": 0.25,
"phonetic_glossary": {
"WAL": "write-ahead log",
"TTL": "T T L",
"HTTP 429": "H T T P four twenty-nine"
},
"scenes": [
{
"id": "act1",
"beats": [
{
"id": "act1_ingress",
"min_visual_s": 3.2,
"text": "Inbound writes terminate at the gateway before touching stateful storage."
},
{
"id": "act1_bucket",
"min_visual_s": 3.8,
"text": "An atomic token-bucket check absorbs burst traffic while bounding database concurrency."
},
{
"id": "act1_reject",
"min_visual_s": 3.5,
"text": "When burst tokens drain to zero, excess callers fail fast with HTTP 429."
}
]
}
]
}
- 패스 1 — 비트별 WAV 합성 및 정확한 타이밍 방출:
# 옵션 A: 오프라인 Piper ONNX 신경망 TTS (네트워크 호출 없음)
python3 .agents/skills/manim-video-explainer/mux_narration.py \
--manifest tmp/narration_manifest.json \
--scene-id act1 \
--wav-dir cache/beats_en \
--piper-bin ./venv/bin/piper \
--model ~/.cache/piper-voices/en_US-lessac-medium.onnx \
--emit-timings tmp/beat_timings.json
# 옵션 B: OpenAI 호환 HTTP 신경망 TTS (/v1/audio/speech, 표준 라이브러리 urllib)
python3 .agents/skills/manim-video-explainer/mux_narration.py \
--manifest tmp/narration_manifest.json \
--scene-id act1 \
--wav-dir cache/beats_en \
--http-tts-url https://api.openai.com/v1/audio/speech \
--http-tts-key-env OPENAI_API_KEY \
--http-tts-model gpt-4o-mini-tts \
--http-tts-voice cedar \
--http-tts-instructions "Calm, clear technical documentary narrator; brief pauses at clauses." \
--emit-timings tmp/beat_timings.json
(모든 커스텀 CLI 합성기는 --tts-cmd "my-tts --out {wav} --text {text}"를 통해 사용하거나, 미리 렌더링된 .wav 파일을 --wav-dir에 직접 배치할 수도 있습니다.)
tmp/beat_timings.json으로 구동되는 1080p60 Manim 비디오 렌더링:
./venv/bin/manim -qh --fps 60 scenes.py Act1RateLimiter
- 패스 2 — 샘플 정확도 마스터 트랙 조립, 음량 정규화, 테일 가드 및 AAC 먹싱:
python3 .agents/skills/manim-video-explainer/mux_narration.py \
--manifest tmp/narration_manifest.json \
--scene-id act1 \
--wav-dir cache/beats_en \
--video media/videos/scenes/1080p60/Act1RateLimiter.mp4 \
--loudnorm-lufs -18.0 \
--output output/act1_en.mp4 \
--vtt output/act1_en.vtt
mux_narration.py는 각 비트 WAV를 round(start_s * sample_rate) 위치의 16비트 PCM 마스터 트랙에 배치하고, 최종 음성이 비디오 지속 시간을 초과할 때마다 tpad=stop_mode=clone을 통해 최종 비디오 프레임을 확장하며(단어 잘림 없음), 선택적으로 EBU R128 음량 정규화(--loudnorm-lufs -18.0)를 적용하여 챕터 간 볼륨 일관성을 유지하고, -c:a aac -b:a 160k -movflags +faststart로 인코딩하며, output/act1_en.vtt를 작성합니다.
Bad-vs-Good 코멘터리 변환, 비영어 발음 오버라이드, 이중 언어 오디오 워크플로우는 references/voiceover-and-commentary.md를 참조하세요.
4. 웹 인코딩, 다중 장면 연결 및 오프라인 HTML5 플레이어
웹 호환 인코딩 및 마스터 릴 연결
무음 워크스루의 경우 (mux_narration.py 없이), 범용 브라우저/QuickTime 호환성(yuv420p + 이동된 moov 원자)을 보장하세요:
ffmpeg -y -i media/videos/scenes/1080p60/Act1RateLimiter.mp4 \
-c:v libx264 -pix_fmt yuv420p -crf 23 -movflags +faststart \
output/act1_silent.mp4
내레이션이 포함된 여러 액트 MP4를 연속 마스터 릴로 조립하려면:
printf "file 'act1_en.mp4'\nfile 'act2_en.mp4'\nfile 'act3_en.mp4'\n" > output/concat.txt
ffmpeg -y -f concat -safe 0 -i output/concat.txt \
-c:v libx264 -pix_fmt yuv420p -crf 21 -c:a aac -b:a 160k -movflags +faststart \
output/master_en.mp4
선택적 독립형 HTML5 시네마 플레이어
다중 챕터 워크스루 또는 이중 언어 설명 패키지를 제공할 때:
assets/video_player_template.html을output/index.html로 복사합니다.PLAYER_CONFIG(chapters,sources,beat_timings.json의 비트 타임스탬프와 일치하는markers)를 커스터마이즈합니다. 단일 언어를 제공하는 경우#langGroup에서 보조 언어 버튼을 숨기거나 생략합니다.- 모든 스타일, 흑백 SVG 아이콘 및 스크립트를 100% 인라인 상태로 유지하여 번들이 외부 CDN 없이 오프라인에서 작동하도록 합니다.
- HTTP
206 Partial Content제공 참고: 브라우저는<video>타임라인을 탐색하고<track>.vtt파일을 깔끔하게 로드하기 위해 HTTP 바이트-레인지(206) 응답이 필요합니다 (file://는 CORS를 통해 WebVTT 트랙을 차단하며, Python의 기본http.server는 바이트-레인지를 지원하지 않습니다 —python3 -m http.server 8080과 같은 적절한 HTTP 서버를 사용하세요).
5. 린트 및 검증
출력 패키지를 제공하기 전에 항상 lint.py를 실행하세요:
python3 .agents/skills/manim-video-explorer/lint.py output/act1_en.mp4 \
--vtt output/act1_en.vtt \
--player output/index.html
lint.py는 다음을 확인합니다:
- 비디오 스트림: H.264 메인/하이 프로파일,
yuv420p,1920x1080. - 오디오 스트림: AAC-LC, 존재 여부.
- WebVTT: 올바른
WEBVTT헤더, 단조로운00:00:00.000 --> 00:00:03.200큐 타임라인. - HTML5 플레이어: CDN URL 없음(
/cdn/,https://,http://없음), 이모지 없음, 인라인style/script태그, 콘텐츠 보안 정책 호환. 에러는 stderr로, 패스는 stdout으로 출력합니다.
6. 저장 및 ".gitignore" 고려 사항
tmp/디렉토리:gitignore에 추가하세요.mux_narration.py가 비트를 해시하는cache/디렉토리도 마찬가지입니다.media/디렉토리: Manim의 기본 비디오/부분 렌더 출력. 일반적으로gitignore에 추가합니다.output/디렉토리: 최종 MP4, VTT, HTML5 플레이어.git lfs로 추적하거나 배포 아티팩트로 간주합니다.scenes.py및narration_manifest.json: Git에 커밋합니다. 이것들은 재현 가능한 소스입니다.
일반적인 워크플로우 요약
- 스토리보드: 설명을 액트와 비트로 나눕니다.
narration_manifest.json을 작성합니다. - Pass 1 (오디오): TTS를 사용하여 비트 WAV를 합성합니다.
beat_timings.json을 생성합니다. - 코드:
nord_manim_theme.py의 프리미티브를 사용하여 ManimScene을 작성합니다. 경계를 확인합니다 (check_scene_bounds.py). - 렌더: Manim을
-qh --fps 60으로 실행합니다. - Pass 2 (먹싱):
mux_narration.py로 오디오, 비디오, 자막을 결합합니다. 필요시 음량 정규화(--loudnorm-lufs)를 적용합니다. - 연결: 여러 액트를 최종 마스터 릴로 결합합니다.
- 패키징: HTML5 플레이어 템플릿을 복사하고
PLAYER_CONFIG를 링크합니다. - 린트:
lint.py로 출력의 완전성과 호환성을 확인합니다.