manim-video-creator

Manim Community ライブラリを使用して、TTSナレーション・BGM付きのアニメーション動画を作成します。

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "manim-video-creator" with this command: npx skills add y-ymmt/cc-manim-video-creator-plugin/y-ymmt-cc-manim-video-creator-plugin-manim-video-creator

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

  1. プラグインディレクトリ(フォールバック)

プロジェクトにエンディング動画がない場合、共通のエンディング動画を使用:

${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 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サイト:

  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 でキャッシュ問題を回避

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

youtube-uploader

No summary provided by upstream source.

Repository SourceNeeds Review
General

Youtube Podcast Generator

Extracts the original text of Youtube video and converts it into a multi-voice AI podcast using a local Node.js API and FFmpeg. It also can show you the text...

Registry SourceRecently Updated
General

ERPClaw

AI-native ERP system with self-extending OS. Full accounting, invoicing, inventory, purchasing, tax, billing, HR, payroll, advanced accounting (ASC 606/842,...

Registry SourceRecently Updated
General

Whisper AI Audio to Text Transcriber

Turn raw transcripts into structured summaries, meeting minutes, and action items.

Registry SourceRecently Updated