🎬

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を選んでくれる。

たとえば「ま」は:

  1. 唇を閉じる瞬間(Shape A)
  2. 「あ」の口(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の出力を組み合わせて、実際に口が動くか確認する。


シリーズを追いかける

→ 著者ページ(Zenn)

Discussion