キャラクターの台詞
立ち絵、字幕、音声、口パク。
台詞のシーンは 3 つのコンポーネントで構成され、React と JSON の両方で使えます。
- キャラクターは表情ごとの画像やレイヤー付き PSD の立ち絵、任意の口パク用の口、字幕の見た目を定義します。
- キャラクタービューは立ち絵を画面に配置します。
- 台詞は字幕を表示し、音声を再生し、表示中のビューの表情や口を変更できます。
React で最初の会話を作る
以下のファイルの隣に、立ち絵 2 枚と音声 2 本を保存します。
import * as React from 'react';
import { Assets, Composition, Rect, Sequence } from '@celesta/react';
import { Character, CharacterView, Dialogue } from '@celesta/character';
import type { AssetReference } from '@celesta/react';
import type { CharacterViewReference } from '@celesta/character';
const mira = React.createRef<AssetReference>();
const miraView = React.createRef<CharacterViewReference>();
export default function Root() {
return (
<Composition width={1920} height={1080} fps={30} durationInFrames={180}>
<Rect width={1920} height={1080} fill="#20243a" />
<Assets>
<Character
ref={mira}
name="Mira"
portrait={{
defaultExpression: 'calm',
expressions: {
calm: './mira/calm.png',
smile: './mira/smile.png',
},
}}
subtitle={{
x: 960, y: 940, anchorX: 0.5, anchorY: 0.5, maxWidth: 1600,
style: {
fontSize: 56,
fill: { type: 'solid', color: '#ffffff' },
stroke: { paint: { type: 'solid', color: '#3a2d52' }, width: 6 },
align: 'center',
},
}}
/>
</Assets>
<CharacterView ref={miraView} character={mira} x={1400} y={200} />
<Sequence durationInFrames={90}>
<Dialogue character={miraView} audio="./voices/line-01.wav">
Welcome back. Shall we begin?
</Dialogue>
</Sequence>
<Sequence from={90} durationInFrames={90}>
<Dialogue character={miraView} expression="smile"
audio="./voices/line-02.wav">
Every scene starts with a single line.
</Dialogue>
</Sequence>
</Composition>
);
}
- キャラクターを宣言する場所は
<Assets>の内側です。ref を通じてビューから参照します。subtitleはTextと同じ位置・スタイルを取ります。 <CharacterView>でビューを配置します。台詞が指定しない間はdefaultExpressionを表示します。ほかのレイヤーと同じくx、y、scale、アンカーを取ります。<Dialogue>をSequenceで囲み、各台詞の時間を指定します。有効な間だけ字幕、音声、expressionが適用され、その後ビューは既定の状態に戻ります。
Audio と同様に volume、startFrom、playbackRate、muted で台詞を調整します。Dialogue の x、y、opacity は字幕を移動・フェードさせ、Transition の中で便利です。
Dialogue の子の中で <Span> を使うと、台詞の一部を強調できます。subtitle.render には書いたままの台詞が content として渡されるので、<Text style={style}>{content}</Text> と描けば強調が保たれます。text は同じ台詞のただの文字列です。
自動の口パク
loadLipSync() は音声を分析し、発話区間に文字起こしの母音を配分して、各時点の口の形(a、i、u、e、o、closed)を生成します。prepare() から呼び出し、Dialogue または CharacterView に lipSync として渡します。
import * as React from 'react';
import { Assets, Composition } from '@celesta/react';
import { Character, CharacterView, Dialogue, loadLipSync, loadPsdPreset } from '@celesta/character';
import type { AssetReference } from '@celesta/react';
import type { CharacterViewReference, LipSyncTrack } from '@celesta/character';
const FPS = 30;
const VOICE = './voices/hello.wav';
const LINE = 'こんにちは、はじめまして!';
const hana = React.createRef<AssetReference>();
const hanaView = React.createRef<CharacterViewReference>();
// Filled once by prepare(), before the first frame renders.
let pose: string[] = [];
let voice: LipSyncTrack | null = null;
export async function prepare() {
pose = await loadPsdPreset({ src: './hana/hana.pfv', favorite: 'smile' });
voice = await loadLipSync({ src: VOICE, text: LINE });
}
export default function Root() {
const seconds = (voice?.durationInSeconds ?? 2) + 0.5;
return (
<Composition width={1920} height={1080} fps={FPS}
durationInFrames={Math.ceil(seconds * FPS)}>
<Assets>
<Character
ref={hana}
name="Hana"
portrait={{
type: 'psd',
src: './hana/hana.psd',
layers: pose,
lipSync: {
a: 'face/mouth/a', i: 'face/mouth/i', u: 'face/mouth/u',
e: 'face/mouth/e', o: 'face/mouth/o',
closed: 'face/mouth/closed',
},
}}
subtitle={{
x: 960, y: 960, anchorX: 0.5, anchorY: 0.5,
style: {
fontSize: 60, align: 'center',
fill: { type: 'solid', color: '#ffffff' },
stroke: { paint: { type: 'solid', color: '#c2577a' }, width: 6 },
},
}}
/>
</Assets>
<CharacterView ref={hanaView} character={hana} x={1300} y={120} />
{voice && (
<Dialogue character={hanaView} audio={VOICE} lipSync={voice}>
{LINE}
</Dialogue>
)}
</Composition>
);
}
- 音声は非圧縮 WAV が必要です。母音を読めるよう、文字起こしはかなまたはローマ字で書いてください。漢字はスキップされます。
- PSD の立ち絵は
face/mouth/aのようなパスで口のレイヤーを指定します。立ち絵ファイルは全フォルダが非表示のことが多いので、layersにレイヤーパス配列、PSDTool の状態文字列、またはloadPsdPreset()で読み込んだ.pfvのお気に入りを指定して表示レイヤーを選びます。 - 画像の立ち絵も口パクできます。
portrait.lipSyncに、形ごとに立ち絵と同じサイズの透明な口画像を指定します。こうすると重ねたときに位置が揃います。 - 手動で口を動かすには、
CharacterViewまたはDialogueにmouth="a"(または別の形)を渡します。useLipSync(track)で現在の形を取得し、別の場所で使えます。
会話を自動で並べる
prepare() で planDialogue(lines, { fps, gap, sceneLeadIn }) を呼ぶと、音声の長さを測って台詞を順に並べます。durationInFrames を指定した台詞は計測しません。gap と leadIn は秒、durationInFrames はフレーム単位です。各台詞には audio または正の durationInFrames が必要です。Composition の長さを plan から設定し、DialogueSeries の views で話者名と表示先を対応付けます。演出のタイミングには plan の line()、startOf()、range()、scene() を使えます。
import * as React from 'react';
import { Assets, Composition, Rect } from '@celesta/react';
import { Character, CharacterView, DialogueSeries, planDialogue } from '@celesta/character';
import type { AssetReference } from '@celesta/react';
import type { CharacterViewReference, DialoguePlan } from '@celesta/character';
const mira = React.createRef<AssetReference>();
const miraView = React.createRef<CharacterViewReference>();
let plan: DialoguePlan;
export async function prepare() {
plan = await planDialogue([
{ id: 'hello', speaker: 'mira', text: 'Welcome back. Shall we begin?',
audio: './voices/line-01.wav' },
{ id: 'start', speaker: 'mira', text: 'Every scene starts with a single line.',
audio: './voices/line-02.wav', expression: 'smile' },
], { fps: 30, gap: 0.25 });
}
export default function Root() {
return (
<Composition width={1920} height={1080} fps={30} durationInFrames={plan.durationInFrames}>
<Rect width={1920} height={1080} fill="#20243a" />
<Assets>
<Character
ref={mira}
name="Mira"
portrait={{
defaultExpression: 'calm',
expressions: {
calm: './mira/calm.png',
smile: './mira/smile.png',
},
}}
subtitle={{
x: 960, y: 940, anchorX: 0.5, anchorY: 0.5, maxWidth: 1600,
style: {
fontSize: 56,
fill: { type: 'solid', color: '#ffffff' },
stroke: { paint: { type: 'solid', color: '#3a2d52' }, width: 6 },
align: 'center',
},
}}
/>
</Assets>
<CharacterView ref={miraView} character={mira} x={1400} y={200} />
<DialogueSeries plan={plan} views={{ mira: miraView }} holdThroughGap />
</Composition>
);
}
holdThroughGap は、台詞の後の間も字幕と表情を保持します。holdSubtitle は、subtitle.render で描く字幕の背景を台詞の間も表示し、held でその状態を判別できます。subtitle.render には text、character(displayName を含む)、metrics、style、maxWidth、frame、durationInFrames が渡されるので、文字の大きさに合う字幕パネルを作れます。全体の例はリポジトリの with-dialogue-series.tsx を参照してください。
VOICEVOX や AivisSpeech で生成した音声には、WAV に文字起こしの母音を均等に割り振る代わりに、合成に使った AudioQuery と @celesta/voicevox または @celesta/aivis-speech を利用できます。
実際の素材で確認
packages/cli/examples/with-dialogue-series.tsx は音声の長さに合わせた会話、packages/cli/examples/with-lip-sync.tsx は PSD 立ち絵の口パクを示すサンプルです。GitHub の Code Download ZIP からソースを取得し、素材を含むアーカイブ全体を展開して、File Open… で開いてください。ソースからのビルドは不要です。