manim-video-creator
npx skills add https://github.com/y-ymmt/cc-manim-video-creator-plugin --skill manim-video-creator
Agent 安装分布
Skill 文档
Manim åç»ã¯ãªã¨ã¤ã¿ã¼
Manim Community ã©ã¤ãã©ãªã使ç¨ãã¦ãTTSãã¬ã¼ã·ã§ã³ã»BGMä»ãã®ã¢ãã¡ã¼ã·ã§ã³åç»ã使ãã¾ãã
åç»ä½æåã®å¿ é ãã¢ãªã³ã°
éè¦: åç»ä½æãéå§ããåã«ãå¿
ã AskUserQuestion ãã¼ã«ã使ç¨ãã¦ä»¥ä¸ã®æ
å ±ããã¢ãªã³ã°ãã¦ãã ããã
ãã¢ãªã³ã°é ç®
AskUserQuestionã§ä»¥ä¸ã確èªï¼
1. åç»ã®ç¨®é¡
- è§£èª¬ã»æè²åç»ï¼è«æè§£èª¬ããã¥ã¼ããªã¢ã«çï¼
- ãã¬ã¼ã³ãã¼ã·ã§ã³åç»
- ãã´ã¢ãã¡ã¼ã·ã§ã³
- ã¤ã³ãã©ã°ã©ãã£ãã¯ã»ãã¼ã¿å¯è¦å
- ã¢ã«ã´ãªãºã ã»ã³ã¼ãå¯è¦å
- ãã®ä»
2. 使ç¯å²
- Manimåç»ã®ã¿ï¼é³å£°ãªãï¼
- Manimåç» + å°æ¬
- ãã«çï¼Manim + TTSãã¬ã¼ã·ã§ã³ + BGMï¼
3. ãã¬ã¼ã·ã§ã³é³å£°ï¼ãã«çã®å ´åï¼
- æ¥æ¬èªå¥³æ§ï¼ja-JP-NanamiNeuralï¼- æ¨å¥¨
- æ¥æ¬èªç·æ§ï¼ja-JP-KeitaNeuralï¼
- è±èªå¥³æ§ï¼en-US-JennyNeuralï¼
- è±èªç·æ§ï¼en-US-GuyNeuralï¼
4. BGMã®ç¨®é¡ï¼ãã«çã®å ´åï¼
- èªåçæï¼ã¢ã³ãã¨ã³ãï¼- è使¨©ããªã¼
- BGMãªã
- å¤é¨BGMãå¾ãã追å
5. ãã©ãããã©ã¼ã /ã¢ã¹ãã¯ãæ¯
- YouTubeï¼16:9, 1920x1080ï¼- æ¨å¥¨
- YouTube Shorts/TikTokï¼9:16, 1080x1920ï¼
- Instagramæç¨¿ï¼1:1, 1080x1080ï¼
- ã«ã¹ã¿ã
ã¯ã¼ã¯ããã¼æ¦è¦
ã¹ãã¼ã¸1: Manimåç»ä½æ
- ãã¬ã¼ã·ã§ã³å°æ¬ãå ã«ä½æããåã»ã°ã¡ã³ãã®é·ããæ¸¬å®
- ã¿ã¤ãã³ã°ãè¨ç®ãã¦Manimã·ã¼ã³ãè¨è¨
- ã·ã¼ã³ã¹ã¯ãªããã使ï¼åã»ã¯ã·ã§ã³ã®éå§ã»çµäºæéãã³ã¡ã³ãã§æç¤ºï¼
- ä½å質ã§ãã¬ãã¥ã¼ã¬ã³ããªã³ã° â ã¿ã¤ãã³ã°ç¢ºèª
- é«åè³ªã§æçµã¬ã³ããªã³ã°
ã¹ãã¼ã¸2: é³å£°çæ
- edge-ttsã§ãã¬ã¼ã·ã§ã³é³å£°ãçæ
- åã»ã°ã¡ã³ããæ£ç¢ºãªã¿ã¤ã ã¹ã¿ã³ãã§é ç½®
ã¹ãã¼ã¸3: é³å£°ã»åç»åæ
- BGMãçæã¾ãã¯æºå
- ãã¬ã¼ã·ã§ã³ã¨BGMãåæï¼BGMé³é: -18dBæ¨å¥¨ï¼
- ffmpegã§åç»ã¨é³å£°ãåæ
éè¦: ã¿ã¤ãã³ã°åæã®ãã¹ããã©ã¯ãã£ã¹
ãã¬ã¼ã·ã§ã³å è¡è¨è¨
åç»ã¨ãã¬ã¼ã·ã§ã³ã®ãããé²ãããããã¬ã¼ã·ã§ã³å°æ¬ãå ã«ä½æãããã®é·ãã«åºã¥ãã¦åç»ã®ã¿ã¤ãã³ã°ãè¨è¨ãã¾ãã
# ã¹ããã1: ãã¬ã¼ã·ã§ã³å°æ¬ã使ããåã»ã°ã¡ã³ãã®é·ããæ¸¬å®
NARRATIONS = [
"æåã®ãã¬ã¼ã·ã§ã³ã", # 測å®çµæ: 3.5ç§
"2çªç®ã®ãã¬ã¼ã·ã§ã³ã", # 測å®çµæ: 4.2ç§
]
# ã¹ããã2: ã¿ã¤ãã³ã°æ§æãè¨è¨
"""
ã¿ã¤ãã³ã°æ§æ:
- ã»ã¯ã·ã§ã³1: 0.0 - 4.0ç§ï¼ãã¬ã¼ã·ã§ã³1 + ä½ç½ï¼
- ã»ã¯ã·ã§ã³2: 4.0 - 9.0ç§ï¼ãã¬ã¼ã·ã§ã³2 + ä½ç½ï¼
"""
# ã¹ããã3: ã·ã¼ã³ã«åæ
class MyScene(Scene):
"""
ã¿ã¤ãã³ã°æ§æï¼ãã¬ã¼ã·ã§ã³åæçï¼:
- ã»ã¯ã·ã§ã³1: 0.0 - 4.0ç§
- ã»ã¯ã·ã§ã³2: 4.0 - 9.0ç§
"""
def construct(self):
self.section1() # 4ç§
self.section2() # 5ç§
def section1(self):
"""ã»ã¯ã·ã§ã³1: 0.0 - 4.0ç§
ãã¬ã¼ã·ã§ã³ (0.5ç§éå§, 3.5ç§): æåã®ãã¬ã¼ã·ã§ã³ã
"""
# 0.0-1.5ç§: ã¿ã¤ãã«è¡¨ç¤º
self.play(Write(title), run_time=1.5)
# 1.5-4.0ç§: å¾
æ©ï¼ãã¬ã¼ã·ã§ã³çµäºãå¾
ã¤ï¼
self.wait(2.5)
# ç´¯è¨: 4.0ç§
ã¢ãã¡ã¼ã·ã§ã³æéã®è¨ç®å¼
# åºæ¬å¼
å¾
æ©æé = ãã¬ã¼ã·ã§ã³çµäºæé - ç¾å¨ã®ç´¯è¨ã¢ãã¡ã¼ã·ã§ã³æé
# ä¾: ãã¬ã¼ã·ã§ã³ã8.5ç§ã§çµäºãç¾å¨ã®ã¢ãã¡ã¼ã·ã§ã³ã6ç§ã¾ã§é²ãã§ããå ´å
self.wait(8.5 - 6.0) # = 2.5ç§å¾
æ©
ã·ã¼ã³ã®ããã¥ã¡ã³ãå½¢å¼
åã»ã¯ã·ã§ã³ã«ä»¥ä¸ã®æ å ±ãã³ã¡ã³ãã§æç¤ºãã¦ãã ããï¼
def show_section(self):
"""ã»ã¯ã·ã§ã³å: éå§æé - çµäºæéï¼æè¦æéï¼
ãã¬ã¼ã·ã§ã³1 (éå§ç§, é·ã): ããã¹ã...
ãã¬ã¼ã·ã§ã³2 (éå§ç§, é·ã): ããã¹ã...
"""
# ã¿ã¤ã ã¹ã¿ã³ãã³ã¡ã³ã
# 0.0-1.0ç§: ã¢ãã¡ã¼ã·ã§ã³èª¬æ
self.play(...)
# 1.0-3.0ç§: å¾
æ©
self.wait(2)
# ç´¯è¨: 3.0ç§
ã¯ã¤ãã¯ã¹ã¿ã¼ã
ããã¸ã§ã¯ãã»ããã¢ãã
# uvã§ããã¸ã§ã¯ãã使
uv init --python 3.12 my-animation
cd my-animation
uv add manim
# é³å£°å¦çç¨ï¼ãã«çï¼
uv add edge-tts pydub
# ã·ã¹ãã ä¾åããã±ã¼ã¸ã®ã¤ã³ã¹ãã¼ã«
# macOS
brew install pkg-config cairo pango ffmpeg
brew install --cask mactex # LaTeXãµãã¼ãç¨
# Linux (Ubuntu/Debian)
# sudo apt-get install libcairo2-dev libpango1.0-dev ffmpeg texlive-full
# Windows
# 1. MiKTeX ãã¤ã³ã¹ãã¼ã«: https://miktex.org/download
# 2. FFmpeg ãã¤ã³ã¹ãã¼ã«: https://ffmpeg.org/download.html
# 3. ãã¹ãç°å¢å¤æ°ã«è¿½å
# ã¤ã³ã¹ãã¼ã«ç¢ºèª
uv run manim checkhealth
åºæ¬çãªã·ã¼ã³æ§é
from manim import *
# æ¥æ¬èªãã©ã³ãè¨å®
config.font = "Hiragino Sans" # macOS
# config.font = "Noto Sans CJK JP" # Linux
# config.font = "Yu Gothic" # Windows
# ãã¼ã¯ã¢ã¼ãèæ¯ï¼æ¨å¥¨ï¼
config.background_color = "#1a1a2e"
# ã«ã©ã¼ãã¬ãã
PRIMARY = "#4fc3f7"
SECONDARY = "#81c784"
ACCENT = "#ffb74d"
HIGHLIGHT = "#f06292"
class MyScene(Scene):
def construct(self):
title = Text("ã¿ã¤ãã«", font_size=48, color=PRIMARY)
self.play(Write(title))
self.wait(2)
ã¬ã³ããªã³ã°ã³ãã³ã
# ä½å質ãã¬ãã¥ã¼ï¼é«éï¼- éçºã»ã¿ã¤ãã³ã°ç¢ºèªç¨
uv run manim -ql scene.py MyScene --disable_caching
# é«å質 - æçµåºåç¨
uv run manim -qh scene.py MyScene --disable_caching
# 4Kå質
uv run manim -qk scene.py MyScene
åç»ã¸ã£ã³ã«å¥ã·ã¼ã³æ§æ
1. è§£èª¬ã»æè²åç»ï¼è«æè§£èª¬ãªã©ï¼
class ExplainerScene(Scene):
"""
ã¿ã¤ãã³ã°æ§æ:
- ã¿ã¤ãã«: 0-8ç§
- ã»ã¯ã·ã§ã³1: 8-25ç§
- ã»ã¯ã·ã§ã³2: 25-45ç§
- ã¾ã¨ã: 45-55ç§
- ã¨ã³ãã£ã³ã°: 55-65ç§
"""
def construct(self):
self.show_title()
self.show_section1()
self.show_section2()
self.show_summary()
self.show_ending()
def show_title(self):
"""ã¿ã¤ãã«: 0-8ç§
ãã¬ã¼ã·ã§ã³ (0.5ç§, 7ç§): ã¿ã¤ãã«ã®èª¬æ...
"""
title = Text("ã¿ã¤ãã«", font_size=72, color=PRIMARY, weight=BOLD)
subtitle = Text("ãµãã¿ã¤ãã«", font_size=32, color=WHITE)
subtitle.next_to(title, DOWN, buff=0.5)
# 0.0-1.5ç§: ã¿ã¤ãã«
self.play(Write(title), run_time=1.5)
# 1.5-2.5ç§: ãµãã¿ã¤ãã«
self.play(FadeIn(subtitle), run_time=1)
# 2.5-7.0ç§: å¾
æ©
self.wait(4.5)
# 7.0-8.0ç§: ãã©ã³ã¸ã·ã§ã³
self.play(FadeOut(title), FadeOut(subtitle), run_time=1)
def show_section1(self):
"""ã»ã¯ã·ã§ã³1: 8-25ç§"""
section_title = Text("ã»ã¯ã·ã§ã³1", font_size=42, color=ACCENT)
section_title.to_edge(UP, buff=0.5)
self.play(Write(section_title), run_time=1)
# ... ã»ã¯ã·ã§ã³ã®å
容
self.play(*[FadeOut(mob) for mob in self.mobjects], run_time=1)
def show_summary(self):
"""ã¾ã¨ãã»ã¯ã·ã§ã³"""
title = Text("ã¾ã¨ã", font_size=42, color=ACCENT)
title.to_edge(UP, buff=0.5)
points = VGroup(
Text("⢠ãã¤ã³ã1", font_size=26),
Text("⢠ãã¤ã³ã2", font_size=26),
Text("⢠ãã¤ã³ã3", font_size=26),
).arrange(DOWN, aligned_edge=LEFT, buff=0.5)
points.next_to(title, DOWN, buff=0.8)
points.shift(LEFT * 2)
self.play(Write(title))
for point in points:
self.play(FadeIn(point, shift=RIGHT * 0.3), run_time=0.8)
self.wait(1.5)
def show_ending(self):
"""ã¨ã³ãã£ã³ã°"""
self.play(*[FadeOut(mob) for mob in self.mobjects])
thanks = Text("ãè¦è´ãããã¨ããããã¾ãã", font_size=32, color=GRAY)
self.play(Write(thanks))
self.wait(3)
2. ãã¬ã¼ã³ãã¼ã·ã§ã³åç»
class PresentationScene(Scene):
"""ã¹ã©ã¤ãå½¢å¼ã®ãã¬ã¼ã³åç»"""
def construct(self):
self.slide_title("ãã¬ã¼ã³ã¿ã¤ãã«", "çºè¡¨è
å")
self.slide_bullets("æ¦è¦", ["ãã¤ã³ã1", "ãã¤ã³ã2", "ãã¤ã³ã3"])
self.slide_diagram()
def slide_title(self, title, author):
t = Text(title, font_size=56, color=PRIMARY)
a = Text(author, font_size=28, color=GRAY)
a.next_to(t, DOWN, buff=0.5)
self.play(Write(t), FadeIn(a))
self.wait(2)
self.play(FadeOut(t), FadeOut(a))
def slide_bullets(self, title, bullets):
t = Text(title, font_size=42, color=ACCENT).to_edge(UP)
items = VGroup(*[
Text(f"⢠{b}", font_size=28) for b in bullets
]).arrange(DOWN, aligned_edge=LEFT, buff=0.5)
items.next_to(t, DOWN, buff=0.8).shift(LEFT * 2)
self.play(Write(t))
for item in items:
self.play(FadeIn(item, shift=RIGHT * 0.5))
self.wait(1)
self.wait(1)
self.play(*[FadeOut(mob) for mob in self.mobjects])
3. ãã´ã¢ãã¡ã¼ã·ã§ã³
class LogoAnimation(Scene):
def construct(self):
circle = Circle(radius=1.5, color=BLUE, fill_opacity=0.8)
text = Text("LOGO", font_size=48, color=WHITE)
self.play(GrowFromCenter(circle), run_time=1)
self.play(Write(text), run_time=0.8)
self.play(
circle.animate.scale(1.1),
text.animate.scale(1.1),
rate_func=there_and_back,
run_time=0.5
)
self.wait(1)
4. ããã¼ãã£ã¼ãã»ãµã¤ã¯ã«å³
class CycleFlowScene(Scene):
"""ãµã¤ã¯ã«å³ï¼ThoughtâActionâObservationçï¼"""
def construct(self):
# ããã¯ã¹ä½æ
box1 = RoundedRectangle(width=3, height=1.2, corner_radius=0.15,
fill_color=PRIMARY, fill_opacity=0.3,
stroke_color=PRIMARY, stroke_width=2)
box1.shift(UP * 1.5)
label1 = Text("ã¹ããã1", font_size=22, color=PRIMARY)
label1.move_to(box1.get_center())
box2 = RoundedRectangle(width=3, height=1.2, corner_radius=0.15,
fill_color=SECONDARY, fill_opacity=0.3,
stroke_color=SECONDARY, stroke_width=2)
box2.shift(RIGHT * 3 + DOWN * 0.8)
label2 = Text("ã¹ããã2", font_size=22, color=SECONDARY)
label2.move_to(box2.get_center())
box3 = RoundedRectangle(width=3, height=1.2, corner_radius=0.15,
fill_color=ACCENT, fill_opacity=0.3,
stroke_color=ACCENT, stroke_width=2)
box3.shift(LEFT * 3 + DOWN * 0.8)
label3 = Text("ã¹ããã3", font_size=22, color=ACCENT)
label3.move_to(box3.get_center())
# ç¢å°
arrow1 = Arrow(box1.get_right() + DOWN * 0.2, box2.get_top(), color=WHITE, buff=0.1)
arrow2 = Arrow(box2.get_left(), box3.get_right(), color=WHITE, buff=0.1)
arrow3 = Arrow(box3.get_top() + RIGHT * 0.3, box1.get_left() + DOWN * 0.2, color=WHITE, buff=0.1)
# é çªã«ã¢ãã¡ã¼ã·ã§ã³
self.play(Create(box1), Write(label1), run_time=1)
self.play(Create(arrow1), run_time=0.5)
self.play(Create(box2), Write(label2), run_time=1)
self.play(Create(arrow2), run_time=0.5)
self.play(Create(box3), Write(label3), run_time=1)
self.play(Create(arrow3), run_time=0.5)
self.wait(2)
TTSãã¬ã¼ã·ã§ã³
å©ç¨å¯è½ãªé³å£°
| è¨èª | é³å£°ID | æ§å¥ | ç¹å¾´ |
|---|---|---|---|
| æ¥æ¬èª | ja-JP-NanamiNeural | å¥³æ§ | æçã§èããããï¼æ¨å¥¨ï¼ |
| æ¥æ¬èª | ja-JP-KeitaNeural | ç·æ§ | è½ã¡çãã声 |
| è±èª | en-US-JennyNeural | å¥³æ§ | ããã¥ã©ã« |
| è±èª | en-US-GuyNeural | ç·æ§ | ãããã§ãã·ã§ãã« |
| è±èª | en-US-AriaNeural | å¥³æ§ | ã¨ãã«ã®ãã·ã¥ |
| ä¸å½èª | zh-CN-XiaoxiaoNeural | å¥³æ§ | æ¨æºç |
| éå½èª | ko-KR-SunHiNeural | å¥³æ§ | æ¨æºç |
ãã¬ã¼ã·ã§ã³é·ãã®æ¸¬å®
# measure_audio.py
import asyncio
import edge_tts
from pydub import AudioSegment
import os
VOICE = "ja-JP-NanamiNeural" # ã¾ãã¯é¸æãããé³å£°
NARRATIONS = [
"æåã®ãã¬ã¼ã·ã§ã³ã",
"2çªç®ã®ãã¬ã¼ã·ã§ã³ã",
]
async def measure_duration(text: str, index: int) -> float:
temp_path = f"temp_{index}.mp3"
communicate = edge_tts.Communicate(text, VOICE, rate="+0%")
await communicate.save(temp_path)
audio = AudioSegment.from_mp3(temp_path)
duration = len(audio) / 1000.0
os.remove(temp_path)
return duration
async def main():
print("ãã¬ã¼ã·ã§ã³é³å£°é·ã測å®:")
print("=" * 50)
total = 0
for i, text in enumerate(NARRATIONS):
duration = await measure_duration(text, i)
total += duration
print(f"{i+1}. [{duration:.2f}ç§] {text[:30]}...")
print("=" * 50)
print(f"åè¨: {total:.2f}ç§")
asyncio.run(main())
ã¿ã¤ã ã¹ã¿ã³ãä»ãé³å£°çæ
# generate_audio.py
import asyncio
import edge_tts
from pydub import AudioSegment
import os
VOICE = "ja-JP-NanamiNeural"
# (éå§ç§, ããã¹ã)
NARRATIONS = [
(0.5, "æåã®ãã¬ã¼ã·ã§ã³ã"),
(8.5, "2çªç®ã®ãã¬ã¼ã·ã§ã³ã"),
(16.0, "3çªç®ã®ãã¬ã¼ã·ã§ã³ã"),
]
async def generate_audio_segment(text: str, output_path: str):
communicate = edge_tts.Communicate(text, VOICE, rate="+0%")
await communicate.save(output_path)
async def main():
audio_dir = "audio_segments"
os.makedirs(audio_dir, exist_ok=True)
# åç»ã®ç·æéãæå®
video_duration_ms = 120 * 1000
final_audio = AudioSegment.silent(duration=video_duration_ms)
print("ãã¬ã¼ã·ã§ã³çæä¸...")
for i, (start_time, text) in enumerate(NARRATIONS):
segment_path = f"{audio_dir}/segment_{i:02d}.mp3"
print(f" {i+1}/{len(NARRATIONS)}: [{start_time:.1f}ç§] {text[:30]}...")
await generate_audio_segment(text, segment_path)
segment = AudioSegment.from_mp3(segment_path)
start_ms = int(start_time * 1000)
final_audio = final_audio.overlay(segment, position=start_ms)
final_audio.export("narration.mp3", format="mp3")
print("宿: narration.mp3")
# ã¯ãªã¼ã³ã¢ãã
for i in range(len(NARRATIONS)):
os.remove(f"{audio_dir}/segment_{i:02d}.mp3")
os.rmdir(audio_dir)
asyncio.run(main())
BGMçæã»è¿½å
èªåçæBGMï¼è使¨©ããªã¼ï¼
å¤é¨ãã¦ã³ãã¼ãä¸è¦ã§ãpydubã®ã¿ã§ã¢ã³ãã¨ã³ãBGMãçæã§ãã¾ãã
# generate_bgm.py
import math
import struct
import wave
import os
from pydub import AudioSegment
def generate_ambient_chord(frequencies, duration_ms, sample_rate=44100, amplitude=0.15):
"""è¤æ°ã®å¨æ³¢æ°ãåæãã¦ã¢ã³ãã¨ã³ããªã³ã¼ããçæ"""
n_samples = int(sample_rate * duration_ms / 1000)
samples = []
for i in range(n_samples):
t = i / sample_rate
value = 0
for freq in frequencies:
phase_mod = 0.002 * math.sin(2 * math.pi * 0.1 * t)
value += amplitude * math.sin(2 * math.pi * freq * t * (1 + phase_mod))
samples.append(value / len(frequencies))
return samples
def apply_envelope(samples, attack_ms, decay_ms, sustain_level, release_ms, sample_rate=44100):
"""ADSRã¨ã³ããã¼ããé©ç¨"""
n_samples = len(samples)
attack_samples = int(sample_rate * attack_ms / 1000)
decay_samples = int(sample_rate * decay_ms / 1000)
release_samples = int(sample_rate * release_ms / 1000)
result = []
for i, sample in enumerate(samples):
if i < attack_samples:
envelope = i / attack_samples
elif i < attack_samples + decay_samples:
decay_progress = (i - attack_samples) / decay_samples
envelope = 1.0 - (1.0 - sustain_level) * decay_progress
elif i > n_samples - release_samples:
release_progress = (i - (n_samples - release_samples)) / release_samples
envelope = sustain_level * (1.0 - release_progress)
else:
envelope = sustain_level
result.append(sample * envelope)
return result
def samples_to_wav(samples, filename, sample_rate=44100):
"""ãµã³ãã«ãWAVãã¡ã¤ã«ã«æ¸ãåºã"""
with wave.open(filename, 'w') as wav_file:
wav_file.setnchannels(1)
wav_file.setsampwidth(2)
wav_file.setframerate(sample_rate)
for sample in samples:
sample = max(-1.0, min(1.0, sample))
packed = struct.pack('h', int(sample * 32767))
wav_file.writeframes(packed)
def generate_ambient_bgm(duration_seconds=130, output_path="bgm.mp3"):
"""ã¢ã³ãã¨ã³ãBGMãçæ"""
print("ã¢ã³ãã¨ã³ãBGMãçæä¸...")
sample_rate = 44100
duration_ms = duration_seconds * 1000
# Cã¡ã¸ã£ã¼ç³»ã³ã¼ãé²è¡
chord_progressions = [
[130.81, 164.81, 196.00], # C E G
[146.83, 174.61, 220.00], # D F A
[164.81, 196.00, 246.94], # E G B
[130.81, 164.81, 196.00], # C E G
]
chord_duration_ms = 8000
all_samples = []
for i in range(int(duration_ms / chord_duration_ms) + 1):
chord = chord_progressions[i % len(chord_progressions)]
samples = generate_ambient_chord(chord, chord_duration_ms, sample_rate, amplitude=0.12)
samples = apply_envelope(samples, 2000, 1000, 0.7, 2000, sample_rate)
all_samples.extend(samples)
all_samples = all_samples[:int(sample_rate * duration_seconds)]
# ãã¼ã¹ããã¼ã³è¿½å
print(" ãã¼ã¹ããã¼ã³ã追å ä¸...")
drone_freq = 65.41 # C2
for i in range(len(all_samples)):
t = i / sample_rate
drone = 0.08 * math.sin(2 * math.pi * drone_freq * t)
drone += 0.04 * math.sin(2 * math.pi * drone_freq * 1.5 * t)
all_samples[i] += drone
# ãã§ã¼ãã¤ã³ã»ãã§ã¼ãã¢ã¦ã
print(" ãã§ã¼ãå¦çä¸...")
fade_in_samples = int(sample_rate * 3)
fade_out_samples = int(sample_rate * 5)
for i in range(fade_in_samples):
all_samples[i] *= i / fade_in_samples
for i in range(fade_out_samples):
idx = len(all_samples) - fade_out_samples + i
all_samples[idx] *= (fade_out_samples - i) / fade_out_samples
# WAVã«æ¸ãåºã
temp_wav = "temp_bgm.wav"
samples_to_wav(all_samples, temp_wav, sample_rate)
# MP3ã«å¤æ
audio = AudioSegment.from_wav(temp_wav)
audio.export(output_path, format="mp3", bitrate="128k")
os.remove(temp_wav)
print(f"BGMçæå®äº: {output_path}")
if __name__ == "__main__":
generate_ambient_bgm(130, "bgm.mp3")
ãã¬ã¼ã·ã§ã³ã¨BGMã®åæ
# combine_final.py
from pydub import AudioSegment
import subprocess
import os
def combine_audio_and_video():
"""ãã¬ã¼ã·ã§ã³ã¨BGMãåæããåç»ã¨çµå"""
print("é³å£°ãå¦çä¸...")
narration = AudioSegment.from_mp3("narration.mp3")
bgm = AudioSegment.from_mp3("bgm.mp3")
# BGMããã¬ã¼ã·ã§ã³ã®é·ãã«åããã
if len(bgm) < len(narration):
while len(bgm) < len(narration):
bgm = bgm + bgm
bgm = bgm[:len(narration)]
# BGMé³é調æ´ï¼-18dBæ¨å¥¨ï¼
bgm = bgm - 18
# ãã§ã¼ãã¤ã³ã»ãã§ã¼ãã¢ã¦ã
bgm = bgm.fade_in(3000).fade_out(4000)
# åæ
combined = narration.overlay(bgm)
combined.export("combined_audio.mp3", format="mp3", bitrate="192k")
# åç»ã¨åæ
subprocess.run([
"ffmpeg", "-i", "media/videos/scene/1080p60/MyScene.mp4",
"-i", "combined_audio.mp3",
"-c:v", "copy", "-c:a", "aac", "-b:a", "192k",
"-map", "0:v:0", "-map", "1:a:0",
"-shortest", "-y", "final_output.mp4"
])
os.remove("combined_audio.mp3")
print("宿: final_output.mp4")
if __name__ == "__main__":
combine_audio_and_video()
ã¨ã³ãã£ã³ã°åç»ã®çµå
ãã£ã¬ã¯ããªæ§æã¨æ¤ç´¢åªå é ä½
éè¦: ã¨ã³ãã£ã³ã°åç»ã¯èªåæ¤ç´¢ããã¾ããã¦ã¼ã¶ã¼ã«ãã¹ãèãå¿ è¦ã¯ããã¾ããã
ã¨ã³ãã£ã³ã°åç»ã¯ä»¥ä¸ã®åªå é ä½ã§èªåæ¤ç´¢ããã¾ãï¼
| åªå 度 | å ´æ | ãã¹ |
|---|---|---|
| 1 | ããã¸ã§ã¯ããã£ã¬ã¯ã㪠| ./endings/{aspect_dir}/ending.mp4 |
| 2 | ãã©ã°ã¤ã³ãã£ã¬ã¯ã㪠| ${CLAUDE_PLUGIN_ROOT}/endings/{aspect_dir}/ending.mp4 |
â» {aspect_dir} ã¯åç»ã®ã¢ã¹ãã¯ãæ¯ã«å¿ã㦠16_9ã9_16ã1_1 ã®ãããã
1. ããã¸ã§ã¯ããã£ã¬ã¯ããªï¼åªå ï¼
ããã¸ã§ã¯ãåºæã®ã¨ã³ãã£ã³ã°åç»ãããå ´åï¼
./ # ç¾å¨ã®manimããã¸ã§ã¯ããã£ã¬ã¯ããª
âââ endings/
âââ 16_9/
â âââ ending.mp4
âââ 9_16/
â âââ ending.mp4
âââ 1_1/
âââ ending.mp4
2. ãã©ã°ã¤ã³ãã£ã¬ã¯ããªï¼ãã©ã¼ã«ããã¯ï¼
ããã¸ã§ã¯ãã«ã¨ã³ãã£ã³ã°åç»ããªãå ´åãå ±éã®ã¨ã³ãã£ã³ã°åç»ã使ç¨ï¼
${CLAUDE_PLUGIN_ROOT}/
âââ endings/
âââ 16_9/ # YouTubeç¨ï¼1920x1080ï¼
â âââ ending.mp4
âââ 9_16/ # Shorts/TikTokç¨ï¼1080x1920ï¼
â âââ ending.mp4
âââ 1_1/ # Instagramç¨ï¼1080x1080ï¼
âââ ending.mp4
CLAUDE_PLUGIN_ROOT ç°å¢å¤æ°
CLAUDE_PLUGIN_ROOT ã¯Claude Codeã«ãã£ã¦èªåçã«è¨å®ãããç°å¢å¤æ°ã§ããã©ã°ã¤ã³ã®ã«ã¼ããã£ã¬ã¯ããªãæãã¾ãã
# ç°å¢å¤æ°ã®ç¢ºèª
echo $CLAUDE_PLUGIN_ROOT
# ä¾: ~/.claude/plugins/marketplaces/manim-video-creator/plugins/manim-video-creator
注æ:
- ããã¸ã§ã¯ãåºæã®ã¨ã³ãã£ã³ã°ãããå ´åã¯
./endings/ã«é ç½® - å
±éã®ã¨ã³ãã£ã³ã°ã¯
${CLAUDE_PLUGIN_ROOT}/endings/ã«é ç½® - ã©ã¡ãã«ãã¨ã³ãã£ã³ã°ããªãå ´åã¯ãã¨ã³ãã£ã³ã°ãªãã§åç»ãåºå
ã¨ã³ãã£ã³ã°åç»çµåã¹ã¯ãªãã
# concat_ending.py
import subprocess
import os
import sys
def get_video_dimensions(video_path):
"""åç»ã®å¹
ã¨é«ããåå¾"""
result = subprocess.run([
"ffprobe", "-v", "error",
"-select_streams", "v:0",
"-show_entries", "stream=width,height",
"-of", "csv=p=0",
video_path
], capture_output=True, text=True)
width, height = map(int, result.stdout.strip().split(','))
return width, height
def get_aspect_ratio_dir(width, height):
"""ã¢ã¹ãã¯ãæ¯ã«åºã¥ãã¦ãã£ã¬ã¯ããªåãè¿ã"""
if width > height:
return "16_9"
elif width < height:
return "9_16"
else:
return "1_1"
def find_ending_video(aspect_dir, plugin_root=None):
"""ã¨ã³ãã£ã³ã°åç»ãæ¤ç´¢ï¼ããã¸ã§ã¯ãåªå
ããã©ã°ã¤ã³ãã©ã¼ã«ããã¯ï¼
Args:
aspect_dir: ã¢ã¹ãã¯ãæ¯ãã£ã¬ã¯ããªåï¼16_9, 9_16, 1_1ï¼
plugin_root: ãã©ã°ã¤ã³ã®ã«ã¼ããã£ã¬ã¯ããª
Returns:
ã¨ã³ãã£ã³ã°åç»ã®ãã¹ãè¦ã¤ãããªãå ´åã¯None
"""
# 1. ããã¸ã§ã¯ããã£ã¬ã¯ããªãåªå
project_ending = os.path.join(".", "endings", aspect_dir, "ending.mp4")
if os.path.exists(project_ending):
print(f"ããã¸ã§ã¯ãã®ã¨ã³ãã£ã³ã°åç»ã使ç¨: {project_ending}")
return project_ending
# 2. ãã©ã°ã¤ã³ãã£ã¬ã¯ããªããã©ã¼ã«ããã¯
if plugin_root is None:
plugin_root = os.environ.get("CLAUDE_PLUGIN_ROOT", ".")
plugin_ending = os.path.join(plugin_root, "endings", aspect_dir, "ending.mp4")
if os.path.exists(plugin_ending):
print(f"ãã©ã°ã¤ã³ã®ã¨ã³ãã£ã³ã°åç»ã使ç¨: {plugin_ending}")
return plugin_ending
# ã©ã¡ãã«ãè¦ã¤ãããªã
print(f"è¦å: ã¨ã³ãã£ã³ã°åç»ãè¦ã¤ããã¾ããã§ãã")
print(f" - ããã¸ã§ã¯ã: {project_ending}")
print(f" - ãã©ã°ã¤ã³: {plugin_ending}")
return None
def concat_with_ending(main_video, plugin_root=None):
"""ã¡ã¤ã³åç»ã¨ã¨ã³ãã£ã³ã°åç»ãçµå
Args:
main_video: ã¡ã¤ã³åç»ã®ãã¹
plugin_root: ãã©ã°ã¤ã³ã®ã«ã¼ããã£ã¬ã¯ããªï¼æå®ããªãå ´åã¯ç°å¢å¤æ°ããåå¾ï¼
"""
width, height = get_video_dimensions(main_video)
aspect_dir = get_aspect_ratio_dir(width, height)
# ã¨ã³ãã£ã³ã°åç»ãæ¤ç´¢ï¼ããã¸ã§ã¯ãåªå
ï¼
ending_path = find_ending_video(aspect_dir, plugin_root)
if ending_path is None:
print("ã¨ã³ãã£ã³ã°åç»ãªãã§ç¶è¡ãã¾ã")
return main_video
# çµåãªã¹ãã使
with open("concat_list.txt", "w") as f:
f.write(f"file '{os.path.abspath(main_video)}'\n")
f.write(f"file '{os.path.abspath(ending_path)}'\n")
output_path = main_video.replace(".mp4", "_with_ending.mp4")
# åç»ãçµåï¼åãã³ã¼ããã¯ã®å ´åã¯é«éï¼
subprocess.run([
"ffmpeg", "-f", "concat", "-safe", "0",
"-i", "concat_list.txt",
"-c", "copy", "-y", output_path
])
os.remove("concat_list.txt")
print(f"宿: {output_path}")
return output_path
if __name__ == "__main__":
main_video = sys.argv[1] if len(sys.argv) > 1 else "final_output.mp4"
concat_with_ending(main_video)
注æäºé
- ã¡ã¤ã³åç»ã¨ã¨ã³ãã£ã³ã°åç»ã®ã³ã¼ããã¯ã»è§£å度ã»ãã¬ã¼ã ã¬ã¼ããä¸è´ããã
- ä¸ä¸è´ã®å ´åã¯åã¨ã³ã³ã¼ããå¿ è¦ï¼
ffmpeg -f concat -safe 0 -i concat_list.txt \
-c:v libx264 -preset medium -crf 18 \
-c:a aac -b:a 192k \
-y final_with_ending.mp4
ãã¶ã¤ã³ã¬ã¤ãã©ã¤ã³
æ¨å¥¨ã«ã©ã¼ãã¬ãã
# ãã¼ã¯ã¢ã¼ãï¼æ¨å¥¨ï¼
config.background_color = "#1a1a2e"
PRIMARY = "#4fc3f7" # ã©ã¤ããã«ã¼
SECONDARY = "#81c784" # ã°ãªã¼ã³
ACCENT = "#ffb74d" # ãªã¬ã³ã¸
HIGHLIGHT = "#f06292" # ãã³ã¯
TEXT_COLOR = WHITE
# ã©ã¤ãã¢ã¼ã
config.background_color = WHITE
PRIMARY = "#2563eb"
SECONDARY = "#16a34a"
ACCENT = "#f59e0b"
HIGHLIGHT = "#ec4899"
TEXT_COLOR = "#1f2937"
ãã©ã³ãè¨å®
# æ¥æ¬èªãã©ã³ãï¼OSå¥ï¼
config.font = "Hiragino Sans" # macOS
# config.font = "Noto Sans CJK JP" # Linux
# config.font = "Yu Gothic" # Windows
# æ¨å¥¨ãã©ã³ããµã¤ãº
# ã¡ã¤ã³ã¿ã¤ãã«: 48-72
# ã»ã¯ã·ã§ã³ã¿ã¤ãã«: 36-42
# æ¬æ: 22-28
# ãã£ãã·ã§ã³: 18-22
ãã©ãããã©ã¼ã å¥è¨å®
| ãã©ãããã©ã¼ã | è§£å度 | ã¢ã¹ãã¯ãæ¯ | æå¤§é·ã |
|---|---|---|---|
| YouTube | 1920×1080 | 16:9 | å¶éãªã |
| YouTube Shorts | 1080×1920 | 9:16 | 60ç§ |
| TikTok | 1080×1920 | 9:16 | 10å |
| Instagram Reels | 1080×1920 | 9:16 | 90ç§ |
| Instagram æç¨¿ | 1080×1080 | 1:1 | 60ç§ |
| Twitter/X | 1920×1080 | 16:9 | 2å20ç§ |
åç»ä½æå¾ã®æ³¨æäºé
è使¨©ã«é¢ããéè¦äºé
åç»ä½æå®äºå¾ã以ä¸ã®æ³¨æäºé ãã¦ã¼ã¶ã¼ã«å¿ ãä¼ãã¦ãã ããï¼
ãåç»å©ç¨ã«é¢ããéè¦ãªæ³¨æäºé
ã
1. BGMã«ã¤ãã¦
- ãèªåçæBGMãã使ç¨ããå ´åï¼
â ãã®BGMã¯è使¨©ããªã¼ã§ããåç¨ã»éåç¨åããèªç±ã«å©ç¨ã§ãã¾ãã
- å¤é¨BGMã使ç¨ããå ´åï¼
â å¿
ãå©ç¨è¦ç´ã確èªãã¦ãã ãã
â ããªã¼BGMãµã¤ãã§ããã¯ã¬ã¸ãã表è¨å¿
é ããåç¨å©ç¨ä¸å¯ããªã©ã®
æ¡ä»¶ãããå ´åãããã¾ã
â æ¨å¥¨ããªã¼BGMãµã¤ã:
- DOVA-SYNDROME (https://dova-s.jp/)
- çè¶ã®é³æ¥½å·¥æ¿ (https://amachamusic.chagasi.com/)
- YouTube Audio Library
2. TTSãã¬ã¼ã·ã§ã³ã«ã¤ãã¦
- edge-ttsã§çæããé³å£°ã¯ãMicrosoftã®å©ç¨è¦ç´ã«å¾ãã¾ã
- åç¨å©ç¨ã®å ´åã¯ãAzure Speech Servicesã®ææãã©ã³ãæ¤è¨ãã¦ãã ãã
3. ã³ã³ãã³ãã«ã¤ãã¦
- è«æè§£èª¬ãªã©ã®å ´åãå¼ç¨å
ãæè¨ãã¦ãã ãã
- ä»è
ã®èä½ç©ã使ç¨ããå ´åã¯ãè使¨©æ³ã«å¾ã£ã¦ãã ãã
4. æ¨å¥¨ã¯ã¬ã¸ãã表è¨ä¾
ãã¢ãã¡ã¼ã·ã§ã³: Manim Community
BGM: [BGMã®ã½ã¼ã¹]
ãã¬ã¼ã·ã§ã³: Microsoft Edge TTSã
ãã³ãã¬ã¼ã
- ã·ã¼ã³ãã³ãã¬ã¼ã: scene_template.py
- é³å£°æ¸¬å®: measure_audio.py
- é³å£°çæ: generate_audio.py
ãªãã¡ã¬ã³ã¹
- ã¢ãã¡ã¼ã·ã§ã³: animations.md
- Mobjects: mobjects.md
- ããã¹ã & æ°å¼: text-and-math.md
- 3Dã·ã¼ã³: 3d-scenes.md
- ã°ã©ã: graphing.md
ãã©ãã«ã·ã¥ã¼ãã£ã³ã°
é³å£°ã¨åç»ãããã
- ãã¬ã¼ã·ã§ã³å°æ¬ãå ã«ä½æããåã»ã°ã¡ã³ãã®é·ããæ¸¬å®
- 測å®çµæã«åºã¥ãã¦åç»ã®ã¿ã¤ãã³ã°ãè¨è¨
- åã»ã¯ã·ã§ã³ã®ç´¯è¨æéãã³ã¡ã³ãã§è¿½è·¡
- wait()ã®æéã調æ´ãã¦åæ
ããã¹ããç»é¢ç«¯ã§åãã
- font_sizeãå°ããããï¼æ¥æ¬èªã¯48以䏿¨å¥¨ï¼
- buffå¤ã調æ´ãã¦ãã¼ã¸ã³ã確ä¿
- shift()ã§ä½ç½®ã調æ´
ã¬ã³ããªã³ã°ãé ã
- éçºä¸ã¯
-qlãªãã·ã§ã³ï¼ä½å質ï¼ãä½¿ç¨ - æçµåºåã®ã¿
-qhãä½¿ç¨ --disable_cachingã§ãã£ãã·ã¥åé¡ãåé¿