技術 約3分で読めます

Remotion + VOICEPEAKで実際に解説動画を作った

前回の記事で調べた内容を実際に試した。「検索を速くするデータ構造」の記事を動画化してみた。

完成した動画

11シーン、約2分23秒。Trie、ダブル配列、転置インデックス、接尾辞配列、接尾辞木、BK木、N-gram、Bloom Filter、B+木、LSM木を紹介している。

セットアップ

# Remotionプロジェクト作成
npx degit remotion-dev/template movies-video
cd movies-video
npm install

# 音声解析用
npm install @remotion/media-utils

VOICEPEAKのCLIは /Applications/voicepeak.app/Contents/MacOS/voicepeak で実行可能。

音声生成

VOICEPEAKでナレーションを生成する。

/Applications/voicepeak.app/Contents/MacOS/voicepeak \
  -s "今回は、検索を高速化するためのデータ構造を紹介します。" \
  -n "Japanese Female 1" \
  -o public/audio/intro.wav

利用可能なナレーター一覧は --list-narrator で確認できる。

/Applications/voicepeak.app/Contents/MacOS/voicepeak --list-narrator

リップシンク実装

音声の音量に応じてキャラクターの口を切り替える仕組み。@remotion/media-utilsvisualizeAudio で音量を取得し、閾値を超えたら口を開く。

import { useAudioData, visualizeAudio } from "@remotion/media-utils";

// 口パターン(控えめな開き具合が自然)
const openMouths = ["kana_i.png", "kana_u.png", "kana_e.png"];
const closedMouth = "kana_n.png";

// 音量を取得
const audioData = useAudioData(audioSrc);
const visualization = visualizeAudio({ fps, frame, audioData, numberOfSamples: 32 });
const avgVolume = visualization.reduce((a, b) => a + b, 0) / visualization.length;

// 閾値を超えたら口を開く
const isSpeaking = avgVolume > 0.01;

調整ポイント

  • 口の開閉が速すぎる → 数フレームごとに切り替え(6フレーム間隔)
  • 口を大きく開けすぎると不自然a, o を除外して i, u, e のみ使用
  • 音量のスムージング → 4フレーム分の平均を取る

図解コンポーネント

各データ構造の図解はReact/SVGで実装した。スライド画像を作る必要がない。

// Trieの例
<svg>
  <circle cx={100} cy={50} r={20} fill="#3b82f6" />
  <text x={100} y={55}>c</text>
  <line x1={100} y1={70} x2={60} y2={120} stroke="#666" />
</svg>

アニメーションは useCurrentFrame()interpolate() で制御する。

const frame = useCurrentFrame();
const opacity = interpolate(frame, [0, 15], [0, 1], { extrapolateRight: "clamp" });

シーン構成

Series コンポーネントでシーンを連続再生する。

const scenes = [
  { title: "検索を速くするデータ構造", audioFile: "audio/intro.wav", durationInFrames: 340 },
  { title: "Trie(トライ木)", audioFile: "audio/trie.wav", durationInFrames: 530, slide: <TrieVisualization /> },
  // ...
];

// Seriesで連続再生
<Series>
  {scenes.map((scene, i) => (
    <Series.Sequence key={i} durationInFrames={scene.durationInFrames}>
      <Scene title={scene.title} audioFile={scene.audioFile} slide={scene.slide} />
    </Series.Sequence>
  ))}
</Series>

キャラクター配置

バストアップよりウェストアップ(腰から上)の方が落ち着く。バストアップだと画面の圧が強い。

<div style={{
  position: "absolute",
  right: 0,
  bottom: 0,
  width: 500,
  height: 620,
  overflow: "hidden",  // 下半身を切り取る
}}>
  <Img src={staticFile(mouthImage)} style={{ height: 1100, top: 0 }} />
</div>

レンダリング

# フル版(1080p)
npx remotion render SearchDataStructures out/video.mp4 --codec h264

# Web埋め込み用(720p、軽量)
npx remotion render SearchDataStructures-web out/video-web.mp4 --codec h264 --crf 30
バージョン解像度サイズ
フル版1920x108027 MB
Web版1280x7207.8 MB

所感

  • スライド画像不要: データ構造の図解はSVG/Reactで十分表現できる
  • 口パク調整が重要: 大きく開けすぎない、切り替え頻度を抑える
  • ゆっくり系の立ち絵が流行る理由がわかった: バストアップだと圧が強い

これもうちょっとちゃんとした素材使えばネタだしだけで動画作れるな。