音声合成(TTS)とモーション動画を組み合わせ、待機状態から発話→待機へシームレスに繋がるクリップを生成するためのローカルAPIサーバーです。
本プロジェクトはアルファ版です。各バージョンでインタフェースが後方互換なく変更される可能性があるためご注意ください。
- Node.js 20 以上
- ffmpeg / ffprobe
- TTS エンジン (以下のいずれか):
- VOICEVOX エンジン (ローカルAPI)
- Style-Bert-VITS2 API サーバー
- (任意) STTサーバー: 音声入力の文字起こし機能を使う場合
ローカルの場合は config/example.stream-profile.local.json を、Docker Composeの場合は config/example.stream-profile.docker.json を config/stream-profile.json にコピーしてください。
cp config/example.stream-profile.local.json config/stream-profile.json
npm installモーション素材はプロジェクト直下の motions/ ディレクトリにまとめて配置し、config/stream-profile.json では talk_idle.mp4 や dir_name/talk_idle.mp4 のように motions/ からの相対パス(接頭辞なし)で参照します。まずはサンプルをセットアップしておくと動作確認が容易です。
mkdir -p motions output
cp example/motion/* motions/example/motion/ には Anchor のサンプル素材が入っているので、必要に応じて差し替えてください。生成された MP4/WAV は常に output/ に保存されます。
npm run devhttp://localhost:4000/docs で Swagger UI を確認できます。
docker compose を利用するとローカルの Node.js を汚さずに起動できます。初回は config/example.stream-profile.docker.json を config/stream-profile.json にコピーし、Compose ではモーション素材 (./motions:/app/motions:ro) と出力先 (./output:/app/output) をボリュームマウントしてください。加えて RESPONSE_PATH_BASE=${PWD}/output を環境変数として渡すことで、コンテナが生成したファイルのホスト側フルパスを API レスポンスで受け取れます。サンプルの stream-profile.json もこのディレクトリ構成を前提に、モーションは talk_idle.mp4 / foo/talk_idle.mp4 といった motions/ 内相対パスだけで指定しています。
Compose には VOICEVOX エンジンの voicevox サービス(voicevox/voicevox_engine:cpu-latest)も含まれており、http://voicevox:50021 で待ち受けます。config/stream-profile.json の voicevoxUrl もこのホスト名を参照するようデフォルトで設定しているため、Compose を使わない場合には実行環境に合わせて URL を変更してください。
- ローカルの
src/・config/・motions/・output/をボリュームマウントしたts-node実行環境です。 - 以下で起動できます。
docker compose up animation-streamer-dev
- ソースを編集すると即座に反映されます。
tsconfig.jsonや依存関係を変えた場合はdocker compose build animation-streamer-devで再ビルドしてください。
ghcr.io/0235-jp/animation-streamer:latestを利用し、npm run startでビルド済み成果物を起動するサービスです。- イメージ内には設定ファイルやモーション素材・出力先ディレクトリを含めていないため、必ず
config/・motions/・output/をボリュームマウントしてください。docker compose pull animation-streamer docker compose up animation-streamer
- 生成済みの MP4/WAV は
output/ボリュームに書き出されます。不要になったファイルはホスト側で削除してください (RESPONSE_PATH_BASEを設定していればレスポンスにその絶対パスが返ります)。
両サービスとも http://localhost:4000 で待ち受けます。PORT や HOST を変更したい場合は config/stream-profile.json の server セクションを更新してください。
curl -X POST http://localhost:4000/api/generate \
-H 'Content-Type: application/json' \
-d '{
"stream": false,
"debug": true,
"presetId": "anchor-a",
"requests": [
{ "action": "start" },
{ "action": "speak", "params": { "text": "こんにちは", "emotion": "happy" } },
{ "action": "idle", "params": { "durationMs": 2000 } },
{ "action": "speak", "params": { "text": "さようなら" } }
]
}'stream=false の場合は combined.outputPath に 1 本にまとめたMP4パスが返却されます。 stream=true を指定すると各アクション完了ごとに NDJSON でレスポンスがストリーミングされます。
presetId はリクエスト直下で 必須 指定です(すべてのアクションが同一プリセットを参照します)。
server.apiKey を設定した場合は -H 'X-API-Key: <your-key>' を付与してください。
/api/generate で生成した動画はキャッシュとして再利用できます。
curl -X POST http://localhost:4000/api/generate \
-H 'Content-Type: application/json' \
-d '{
"presetId": "anchor-a",
"cache": true,
"requests": [
{ "action": "speak", "params": { "text": "こんにちは" } }
]
}'cache: true を指定すると、同じ設定・同じテキストのリクエストでは既存のファイルを再利用し、TTS や動画生成をスキップします。
cache: true: ハッシュベースのファイル名(例:abc123...def.mp4)。同一内容は同一ファイル。cache: false(デフォルト): ハッシュ+UUID(例:abc123...def-uuid.mp4)。毎回新規ファイルを生成。- カスタムアクション:
{presetId}-{actionId}.mp4(例:kanon-loop.mp4)。常に固定。
生成された動画の情報は output/output.jsonl に記録されます(JSONL形式)。
{"file":"abc123.mp4","type":"speak","preset":"anchor-a","inputType":"text","text":"こんにちは","emotion":"neutral","tts":"voicevox","speakerId":1,"createdAt":"2024-01-01T00:00:00.000Z"}
{"file":"def456.mp4","type":"idle","preset":"anchor-a","durationMs":2000,"emotion":"neutral","createdAt":"2024-01-01T00:00:00.000Z"}サーバー起動時に、存在しないファイルのログエントリは自動で削除されます。
- キャッシュ対象:
speak(text/audio/audio+transcribe)、idle、結合動画 - キャッシュ対象外:
/api/stream/*(ストリーミング配信用)
RTMP/HTTP-FLV でリアルタイム配信を行う場合は /api/stream/* エンドポイントを使用します。
curl -X POST http://localhost:4000/api/stream/start \
-H 'Content-Type: application/json' \
-d '{ "presetId": "anchor-a" }'debug: true を指定すると output/stream 内のファイルを自動削除しません(デバッグ用)。
配信中に発話を挿入するには /api/stream/text を使用します。フォーマットは /api/generate と同じです。
curl -X POST http://localhost:4000/api/stream/text \
-H 'Content-Type: application/json' \
-d '{
"presetId": "anchor-a",
"requests": [
{ "action": "speak", "params": { "text": "こんにちは" } }
]
}'curl -X POST http://localhost:4000/api/stream/stopOBS のメディアソースに rtmp://localhost:1935/live/main を指定してください。config/stream-profile.json の rtmp.outputUrl でポートやストリームキーを変更できます。
config/stream-profile.json でモーション動画や VOICEVOX エンドポイントなどを定義します。主な項目は以下の通りです。
- server.port / server.host / server.apiKey: API の待受ポート・ホスト・APIキー。
- rtmp.outputUrl: RTMP 出力先 URL(デフォルト:
rtmp://127.0.0.1:1935/live/main)。内蔵の node-media-server がこの URL でストリームを受信し、OBS 等から参照可能にします。 - presets: キャラクターのプリセット定義配列。最低1件登録し、APIからは
presetIdで参照します。- id / displayName: プリセット識別子と任意の表示名。
- actions: プリセット固有のカスタムアクション群(
speak/idleは予約語のため不可)。idはrequests[].actionに指定し、pathはmotions/からの相対パス(例:talk_idle.mp4やdir_name/talk_idle.mp4)です。 - idleMotions / speechMotions: 待機・発話モーションのプール。
large/smallと emotion ごとに最適なクリップを選択し、motionIdで直接指定もできます。 - speechTransitions (任意):
speakの前後に自動で差し込む導入/締めモーション。emotion が一致しない場合はneutral→ その他の順でフォールバックします。 - audioProfile: プリセット単位の TTS 設定。
ttsEngineで使用するエンジンを指定し、emotion 別のvoices[]を定義します(最低1件必須)。 - モーション動画は
motions/以下にまとまっている想定です。設定ファイルからは接頭辞なしのmotions/内相対パスで参照し、Docker では./motions:/app/motions:roをマウントして同じパス構成を維持します。
- 出力ファイルは常にプロジェクト直下の
output/に保存されます(設定不要)。Docker では./output:/app/outputをマウントし、RESPONSE_PATH_BASEにホスト側outputの絶対パスを渡すことで API レスポンスにホスト上のパスを返せます。
config/example.stream-profile.docker.json / config/example.stream-profile.local.json には Anchor のサンプルが含まれているので、必要に応じて presets[] を増やし、presetId を切り替えて利用してください。
speakLipSync アクションは、ベース動画に口画像をオーバーレイ合成し、音素レベルで同期したリップシンク動画を生成します。
- 事前処理(Python): ベース動画から口位置を検出しJSONファイルを出力
- 動画生成(TypeScript): 口位置JSONを読み込み、FFmpegでオーバーレイ合成
| 項目 | speak(既存) | speakLipSync |
|---|---|---|
| 素材 | モーション動画(mp4) | ベースループ動画 + 口画像(png)× 6枚/emotion |
| 口の動き | 動画に含まれる(固定) | 音声に合わせて口画像をオーバーレイ |
| 同期精度 | 音声の長さのみ | 音素レベルで同期 |
| 事前処理 | 不要 | 口位置検出が必要(Python) |
ベース動画から口位置を検出するPythonスクリプトを使用します。mediapipe の FaceLandmarker を使用しています。
# Python 仮想環境を作成
python -m venv venv
source venv/bin/activate
# 依存パッケージをインストール
pip install -r scripts/requirements.txt
# 口位置検出を実行
python scripts/detect_mouth_positions.py \
--input motions/talk_loop.mp4 \
--output motions/talk_loop.mouth.json出力される JSON には各フレームの口の中心座標・サイズが含まれます:
{
"videoFileName": "talk_loop.mp4",
"frameRate": 16,
"positions": [
{ "frameIndex": 0, "centerX": 448, "centerY": 720, "width": 120, "height": 60 }
]
}プリセットに lipSync オブジェクトを追加し、ベース動画・口位置JSON・口画像を指定します。large(長文用)と small(短文用、省略可)の2種類を設定できます。
{
"presets": [{
"id": "anchor-a",
"audioProfile": {
"ttsEngine": "voicevox",
"voicevoxUrl": "http://127.0.0.1:50021",
"voices": [{ "emotion": "neutral", "speakerId": 1 }]
},
"lipSync": {
"large": [
{
"id": "lip-neutral",
"emotion": "neutral",
"basePath": "talk_loop.mp4",
"mouthDataPath": "talk_loop.mouth.json",
"images": {
"A": "lip/neutral_A.png",
"I": "lip/neutral_I.png",
"U": "lip/neutral_U.png",
"E": "lip/neutral_E.png",
"O": "lip/neutral_O.png",
"N": "lip/neutral_N.png"
},
"overlayConfig": {
"scale": 1.0,
"offsetX": 0,
"offsetY": 0
}
}
]
}
}]
}必須フィールド:
basePath: ベースとなるループ動画(motions/からの相対パス)mouthDataPath: Python スクリプトで出力した口位置 JSONimages: aiueoN 形式の口画像(A, I, U, E, O, N)
overlayConfig(オプション):
scale: 口画像のスケール倍率(デフォルト: 1.0)offsetX,offsetY: 位置のオフセット(ピクセル)
images のキー(aiueoN 形式 - 日本語母音ベース):
A: あ - 大きく開いた口I: い - 横に広がった口U: う - すぼめた口E: え - 中間的に開いた口O: お - 丸く開いた口N: ん/無音 - 閉じた口
画像は motions/ ディレクトリ配下に配置します(例: motions/lip/neutral_A.png)。
# テキスト入力
curl -X POST http://localhost:4000/api/generate \
-H 'Content-Type: application/json' \
-d '{
"presetId": "anchor-a",
"requests": [
{ "action": "speakLipSync", "params": { "text": "こんにちは", "emotion": "neutral" } }
]
}'
# 音声入力(STT→TTS)
curl -X POST http://localhost:4000/api/generate \
-H 'Content-Type: application/json' \
-d '{
"presetId": "anchor-a",
"requests": [
{ "action": "speakLipSync", "params": { "audio": { "path": "/path/to/voice.wav", "transcribe": true } } }
]
}'| TTS エンジン | タイムライン生成方式 |
|---|---|
| VOICEVOX | audio_query のモーラ情報(高精度) |
| Style-Bert-VITS2 | MFCC 音声解析 |
| 直接音声使用 | MFCC 音声解析 |
- lipSync 設定必須: プリセットに
lipSync配列がない場合はエラー - 口位置 JSON 必須: Python スクリプトで事前に生成が必要
speak アクションではテキストの代わりに音声ファイルを入力できます。
curl -X POST http://localhost:4000/api/generate \
-H 'Content-Type: application/json' \
-d '{
"presetId": "anchor-a",
"requests": [
{ "action": "speak", "params": { "audio": { "path": "/path/to/voice.wav" } } }
]
}'transcribe: true を指定すると、入力音声を STT で文字起こしし、TTS で再合成します。
curl -X POST http://localhost:4000/api/generate \
-H 'Content-Type: application/json' \
-d '{
"presetId": "anchor-a",
"requests": [
{ "action": "speak", "params": { "audio": { "path": "/path/to/voice.wav", "transcribe": true } } }
]
}'音声の文字起こし機能には OpenAI 互換 API をサポートする STT サーバーが必要です。
推奨: faster-whisper-server
docker run -d -p 8000:8000 fedirz/faster-whisper-server:latest設定ファイルの stt セクションで接続先を指定します:
{
"stt": {
"baseUrl": "http://localhost:8000/v1",
"model": "whisper-1",
"language": "ja"
}
}OpenAI の Whisper API を使う場合:
{
"stt": {
"baseUrl": "https://api.openai.com/v1",
"apiKey": "sk-...",
"model": "whisper-1",
"language": "ja"
}
}audioProfile で使用する TTS エンジンを指定します。voices 配列には最低1件の設定が必要です。
{
"audioProfile": {
"ttsEngine": "voicevox",
"voicevoxUrl": "http://127.0.0.1:50021",
"voices": [
{
"emotion": "neutral",
"speakerId": 1,
"speedScale": 1.1
},
{
"emotion": "happy",
"speakerId": 3,
"pitchScale": 0.3,
"intonationScale": 1.2
}
]
}
}voices のパラメータ:
emotion(必須): 感情ラベル。リクエストのemotionと照合speakerId(必須): VOICEVOX の話者 IDspeedScale,pitchScale,intonationScale,volumeScale: 音声調整パラメータoutputSamplingRate,outputStereo: 出力形式
{
"audioProfile": {
"ttsEngine": "style-bert-vits2",
"sbv2Url": "http://127.0.0.1:5000",
"voices": [
{
"emotion": "neutral",
"modelId": 0,
"speakerId": 0,
"style": "Neutral",
"styleWeight": 1.0,
"sdpRatio": 0.2,
"noise": 0.6,
"noisew": 0.8,
"length": 1.0,
"language": "JP"
},
{
"emotion": "happy",
"modelId": 0,
"speakerId": 0,
"style": "Happy",
"styleWeight": 1.2
}
]
}
}voices のパラメータ:
emotion(必須): 感情ラベルmodelId/modelName: 使用モデルの指定speakerId/speakerName: 話者の指定style,styleWeight: スタイル指定と強度sdpRatio,noise,noisew: 音声のランダム性調整length: 話速(1.0 が基準)language: 言語(JP,EN,ZHなど)
Style-Bert-VITS2 サーバーの起動:
python server_fastapi.pyモーション動画に音声トラック(BGM・効果音など)が含まれている場合、TTS音声とミックスされて出力されます。
- speak アクション: TTS音声とモーション音声を重ね合わせ
- idle アクション: モーション音声があればそのまま維持
- カスタムアクション: モーション音声があればそのまま維持
両方の音声は元の音量のまま合成されます。モーション動画に音声が不要な場合は、事前に音声トラックを削除しておくことをお勧めします。
# 音声トラックを削除する例
ffmpeg -i input.mp4 -c:v copy -an output.mp4モーション動画を連結する際、すべてのファイルで 解像度・フレームレート・コーデック・ピクセルフォーマット が統一されている必要があります。仕様が異なるファイルが混在すると、動画が途中で固まったり乱れたりする原因になります。
起動時に自動で仕様チェックが行われ、不一致がある場合は警告ログと推奨変換コマンドが出力されます。
⚠️ モーション仕様の不一致を検出
--- モーション仕様一覧 ---
[896x1152 16/1fps h264 yuv420p] ← 推奨基準 (最多)
- idle-a-large
- idle-a-small
[1920x1080 24000/1001fps h264 yuv420p]
- talk-a-large
--- 推奨変換コマンド ---
ffmpeg -i "talk_large.mp4" -vf "scale=896:1152,fps=16" -c:v libx264 -pix_fmt yuv420p -an "talk_large_converted.mp4"
多数決で推奨基準が決定され、変換が必要なファイルのコマンドが自動生成されます。
- LipWI2VJs - 口の形の解析に参考にさせていただきました
- MotionPNGTuber - LipSyncアクションの実装に参考にさせていただきました
- wLipSync - MFCCプロファイルデータ提供