Manim(Mathematical Animation Engine)を使用してアニメーション動画を作成します。このスキルは、(1) 解説動画やビジュアライゼーションの作成、(2) プレゼンテーション動画、(3) ロゴアニメーション、(4) インフォグラフィック、(5) 教育コンテンツ、(6) 3Dアニメーション、(7) アルゴリズムやデータ構造の可視化に使用します。2D/3Dシーン、LaTeX数式、グラフ、TTSナレーション、BGMなどをサポートします。
Creates animated educational/presentation videos using Manim with synchronized TTS narration and BGM. Triggers when user requests video creation for tutorials, visualizations, or presentations requiring mathematical animations.
/plugin marketplace add y-ymmt/cc-manim-video-creator-plugin/plugin install manim-video-creator@manim-video-creatorThis skill inherits all available tools. When active, it can use any tool Claude has access to.
references/3d-scenes.mdreferences/animations.mdreferences/graphing.mdreferences/mobjects.mdreferences/text-and-math.mdtemplates/generate_audio.pytemplates/measure_audio.pytemplates/scene_template.pyManim 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: ナレーション台本を作成し、各セグメントの長さを測定
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
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)
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])
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)
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)
| 言語 | 音声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())
外部ダウンロード不要で、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")
# 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 のいずれか
プロジェクト固有のエンディング動画がある場合:
./ # 現在のmanimプロジェクトディレクトリ
└── endings/
├── 16_9/
│ └── ending.mp4
├── 9_16/
│ └── ending.mp4
└── 1_1/
└── ending.mp4
プロジェクトにエンディング動画がない場合、共通のエンディング動画を使用:
${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 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 | 1920x1080 | 16:9 | 制限なし |
| YouTube Shorts | 1080x1920 | 9:16 | 60秒 |
| TikTok | 1080x1920 | 9:16 | 10分 |
| Instagram Reels | 1080x1920 | 9:16 | 90秒 |
| Instagram 投稿 | 1080x1080 | 1:1 | 60秒 |
| Twitter/X | 1920x1080 | 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」
-ql オプション(低品質)を使用-qh を使用--disable_caching でキャッシュ問題を回避