AIで作る業務動画 Day 8|口パクデータの構造を理解してRemotionに組み込む
今日のゴール
Rhubarb Lip Syncが出力する口形状(A〜X)の意味を理解し、Remotionで使えるマッピング表を作成する。
Day 6でRhubarbをインストールし、Day 7では日本語音声7ファイルでリップシンクJSONの生成に成功した。出力されたJSONにはA、B、C...といった記号が並んでいたが、それぞれが何を意味するのか、詳しくは調べていなかった。
今日は、この口形状の意味を掘り下げて、実際にRemotionで使えるようにマッピングを整理する。Day 7で決めた「9種類出力→6種類に正規化」の方針を具体化していく。
9種類の口形状
RhubarbはPreston Blair phoneme systemに基づいた口形状を出力する。これはディズニーやハンナ・バーベラで使われてきた、2Dアニメの事実上の標準。
基本形状(A〜F):必須
| Shape | 名称 | 対応する音 | 視覚的特徴 |
|---|---|---|---|
| A | 閉じた口 | M, B, P | 唇を軽く閉じた状態 |
| B | 少し開いた口 | K, S, T, I | 口がわずかに開き、歯が見える |
| C | 開いた口 | E, AE | 中程度に開いた状態 |
| D | 大きく開いた口 | A, AI | 最も開いた形状 |
| E | 丸めた口 | O, ER | 口がやや丸くなった状態 |
| F | すぼめた口 | U, W, OO | 唇をすぼめた状態 |
拡張形状(G〜H):任意
| Shape | 名称 | 対応する音 | 視覚的特徴 |
|---|---|---|---|
| G | F/V の口 | F, V | 上の歯が下唇に触れている |
| H | 舌が見える口 | L, N, T, D | 舌が歯茎に触れる |
特殊形状(X):任意
| Shape | 名称 | 用途 |
|---|---|---|
| X | 休息/無音 | 発話の前後、ポーズ |
日本語では何種類必要か
9種類すべて用意するのは大変。日本語の場合、いくつか省略できる。
拡張形状(G, H)は不要
-
Shape G:日本語には
/f/、/v/音がほとんどない - Shape H:「ら行」「な行」で有効だが、簡略化してBで代用可能
無音形状(X)はAで代用
XとAは視覚的にほぼ同じ(どちらも閉じた口)。素材を1つ減らせる。
結論:6種類で十分
日本語の5母音(あいうえお)+ 閉じた口 = 6種類。
| Shape | 日本語母音 | 用途 |
|---|---|---|
| A | (閉じ) | 無音、ま行/ば行/ぱ行の子音 |
| B | い | 横に引いた口 |
| C | え | 中程度に開いた口 |
| D | あ | 大きく開いた口 |
| E | お | 丸く開いた口 |
| F | う | すぼめた口 |
Rhubarbのコマンド設定
Day 7で検討した結果、--extendedShapes ""オプション(G, H, Xを無効化)は使わない方針にした。認識精度への影響が懸念されるため、9種類をそのまま出力して、Remotion側で6種類に正規化する。
rhubarb --recognizer phonetic -f json -o output.json input.wav
| オプション | 説明 |
|---|---|
--recognizer phonetic |
日本語対応の音素認識を使用 |
-f json |
JSON形式で出力 |
出力されるShapeはA〜H、Xの9種類。G, H, Xは後述のフォールバックマッピングで6種類に正規化する。
Remotion向けマッピング
RhubarbのJSONをRemotionで使うには、3つのファイルを用意する。
全体の流れ
具体例:「こんにちは!」のリップシンク
実際に「こんにちは!」と発話した音声がどう処理されるか、順を追って説明する。
Step 1: 音声をRhubarbで解析
rhubarb --recognizer phonetic --extendedShapes "" -f json -o konnichiwa.json konnichiwa.wav
Step 2: RhubarbがJSONを出力
「こんにちは!」(約0.8秒)を解析すると、以下のようなJSONが出力される。
{
"metadata": {
"soundFile": "konnichiwa.wav",
"duration": 0.95
},
"mouthCues": [
{ "start": 0.00, "end": 0.05, "value": "A" },
{ "start": 0.05, "end": 0.15, "value": "E" },
{ "start": 0.15, "end": 0.25, "value": "A" },
{ "start": 0.25, "end": 0.35, "value": "B" },
{ "start": 0.35, "end": 0.50, "value": "B" },
{ "start": 0.50, "end": 0.70, "value": "D" },
{ "start": 0.70, "end": 0.95, "value": "A" }
]
}
Step 3: 発音と口形状の対応
| 時間 | 発音 | 口形状 | 理由 |
|---|---|---|---|
| 0.00-0.05 | (開始前) | A | 閉じた口 |
| 0.05-0.15 | こ | E | 「お」の丸い口 |
| 0.15-0.25 | ん | A | 閉じた口 |
| 0.25-0.35 | に | B | 「い」の横に引いた口 |
| 0.35-0.50 | ち | B | 「い」の横に引いた口 |
| 0.50-0.70 | は | D | 「あ」の大きく開いた口 |
| 0.70-0.95 | !(余韻) | A | 閉じた口 |
Step 4: Remotionでフレームごとに画像を決定
30fpsの動画の場合、各フレームでどの画像を表示するか。
| フレーム | 時間(秒) | 口形状 | 表示画像 | 発音 |
|---|---|---|---|---|
| 0 | 0.00 | A | mouth-A.png | (開始) |
| 3 | 0.10 | E | mouth-E.png | こ |
| 6 | 0.20 | A | mouth-A.png | ん |
| 9 | 0.30 | B | mouth-B.png | に |
| 12 | 0.40 | B | mouth-B.png | ち |
| 18 | 0.60 | D | mouth-D.png | は |
| 24 | 0.80 | A | mouth-A.png | ! |
Step 5: 処理の流れ(フレーム18の場合)
フレーム18では「は」の「あ」口(Shape D)が表示され、キャラクターが「は」と発音しているように見える。
| ファイル | 役割 | 依存関係 |
|---|---|---|
| 型定義 | RhubarbのJSONをTypeScriptで扱えるようにする | なし |
| 定数 | 口形状(A-F)と画像ファイルを紐付ける | 型定義を使用 |
| 関数 | フレーム番号から「今どの口形状か」を返す | 型定義を使用 |
この3つがあれば、Remotionコンポーネントで「今のフレームで表示すべき口の画像」を取得できる。
フォルダ構成
project/
├── public/
│ └── character/
│ ├── mouth-A.png # 閉じた口
│ ├── mouth-B.png # い口
│ ├── mouth-C.png # え口
│ ├── mouth-D.png # あ口
│ ├── mouth-E.png # お口
│ └── mouth-F.png # う口
├── src/
│ ├── types/
│ │ └── lipsync.ts # 型定義
│ ├── constants/
│ │ └── mouthShapes.ts # 定数
│ └── utils/
│ └── lipsync.ts # 関数
└── lipsync-data/
└── output.json # Rhubarbの出力
1. 型定義:RhubarbのJSONをTypeScriptに教える
RhubarbのJSON出力をそのまま使うと、TypeScriptは中身の型がわからない。型定義を書いておくと、エディタの補完が効くし、タイポも防げる。
Rhubarbは9種類(A-H, X)を出力するが、実際に使うのは6種類(A-F)なので、両方の型を定義する。
// src/types/lipsync.ts
/** Rhubarbの生出力形状(9種類) */
export type RhubarbShape = 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'X';
/** Remotionで使用する形状(6種類) */
export type MouthShape = 'A' | 'B' | 'C' | 'D' | 'E' | 'F';
/** 1つの口形状キュー(いつからいつまで、どの形状か) */
export interface MouthCue {
start: number; // 開始時間(秒)
end: number; // 終了時間(秒)
value: RhubarbShape; // Rhubarbの生出力
}
/** RhubarbのJSON出力全体 */
export interface RhubarbOutput {
metadata: {
soundFile: string;
duration: number;
};
mouthCues: MouthCue[];
}
/** フォールバックマッピング(G, H, X → A-Fに正規化) */
export const SHAPE_FALLBACK: Record<RhubarbShape, MouthShape> = {
A: 'A',
B: 'B',
C: 'C',
D: 'D',
E: 'E',
F: 'F',
G: 'B', // 閉じた口に近い
H: 'C', // やや開いた口に近い
X: 'A', // 無音なので閉じた口
};
2. 定数:口形状と画像ファイルを紐付ける
口形状(A, B, C...)がわかっても、どの画像を表示すればいいかわからない。定数でマッピングしておく。
// src/constants/mouthShapes.ts
import { MouthShape } from '../types/lipsync';
/** 口形状 → 画像パス のマッピング */
export const MOUTH_SHAPE_IMAGES: Record<MouthShape, string> = {
A: '/character/mouth-A.png',
B: '/character/mouth-B.png',
C: '/character/mouth-C.png',
D: '/character/mouth-D.png',
E: '/character/mouth-E.png',
F: '/character/mouth-F.png',
};
Record<MouthShape, string>は「MouthShapeの各値をキーに持ち、値がstringのオブジェクト」という意味。A-Fすべてのマッピングを定義しないとエラーになるので、漏れを防げる。
3. 関数:フレーム番号から口形状を取得する
Remotionは「今何フレーム目か」を教えてくれる。でもRhubarbのデータは「秒」単位。この変換と、該当するキューの検索、さらにフォールバック正規化を関数にまとめる。
// src/utils/lipsync.ts
import { MouthCue, MouthShape, RhubarbShape, SHAPE_FALLBACK } from '../types/lipsync';
/**
* Rhubarbの出力(9種類)を6種類に正規化
*/
function normalizeShape(shape: RhubarbShape): MouthShape {
return SHAPE_FALLBACK[shape];
}
/**
* 指定フレームでの口形状を取得
* @param cues - Rhubarbの出力(mouthCues配列)
* @param frame - 現在のフレーム番号
* @param fps - フレームレート(通常30)
* @returns 該当する口形状(見つからなければ'A')
*/
export function getMouthShapeAtFrame(
cues: MouthCue[],
frame: number,
fps: number
): MouthShape {
// フレーム番号を秒に変換
const timeInSeconds = frame / fps;
// 該当するキューを探す
const cue = cues.find(
(c) => timeInSeconds >= c.start && timeInSeconds < c.end
);
// 見つからなければ閉じた口(A)を返す
if (!cue) return 'A';
// G, H, X を A-F に正規化して返す
return normalizeShape(cue.value);
}
使い方の例(Remotionコンポーネント内):
const frame = useCurrentFrame(); // 現在のフレーム
const { fps } = useVideoConfig(); // フレームレート
const shape = getMouthShapeAtFrame(mouthCues, frame, fps); // → 'A', 'B', ... (正規化済み)
const imagePath = MOUTH_SHAPE_IMAGES[shape]; // → '/character/mouth-A.png'
素材作成のポイント
口パク素材を作る際の注意点。
| 項目 | 推奨 |
|---|---|
| ファイル形式 | PNG(透過推奨) |
| サイズ | 統一サイズ(例: 341×341px) |
| アンカー | 口の中心を基準に配置 |
各口形状の描き方
| Shape | ポイント |
|---|---|
| A(閉じ) | 唇を軽く閉じる。リラックスした表情 |
| B(い) | 口を横に引き、歯を少し見せる |
| C(え) | 口を開き、舌が少し見える |
| D(あ) | 口を大きく開ける。最大の開口 |
| E(お) | 口を丸く開ける。やや前に突き出す |
| F(う) | 唇をすぼめる。口を前に突き出す |
日本語発音との対応
Rhubarbは音声波形を解析して、自動的に口形状を判定する。日本語の発音ルールを覚える必要はないが、参考までに対応表を載せておく。
母音
日本語は母音で終わる音が多いため、この5つが頻出する。
| 日本語 | Shape | 口の形 |
|---|---|---|
| あ | D | 大きく開く |
| い | B | 横に引く |
| う | F | すぼめる |
| え | C | 中程度に開く |
| お | E | 丸く開く |
子音は気にしなくていい
「ま」「た」「か」などの子音部分は、Rhubarbが自動で適切なShapeを選んでくれる。
たとえば「ま」は:
- 唇を閉じる瞬間(Shape A)
- 「あ」の口(Shape D)
この切り替えをRhubarbが音声から検出してJSONに出力する。私たちがやることは、6種類の口画像を用意するだけ。
今日の成果物
- 口形状(A〜X)の意味を整理
- 日本語向けに6種類を選定
- Remotion向けマッピング表・フォールバック関数を作成
検証環境
| 項目 | バージョン |
|---|---|
| OS | Windows 11 Pro |
| Rhubarb Lip Sync | 1.14.0 |
| 検証日 | 2026-01-01 |
明日のテーマ: キャラクター素材の準備と、音声+リップシンクの統合テスト。Gemini TTSで生成した音声とRhubarbの出力を組み合わせて、実際に口が動くか確認する。
シリーズを追いかける
Discussion