Skip to content

KarakuriAgent/Animation-Streamer

Repository files navigation

Animation Streamer

音声合成(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.jsonconfig/stream-profile.json にコピーしてください。

cp config/example.stream-profile.local.json config/stream-profile.json
npm install

モーション素材はプロジェクト直下の motions/ ディレクトリにまとめて配置し、config/stream-profile.json では talk_idle.mp4dir_name/talk_idle.mp4 のように motions/ からの相対パス(接頭辞なし)で参照します。まずはサンプルをセットアップしておくと動作確認が容易です。

mkdir -p motions output
cp example/motion/* motions/

example/motion/ には Anchor のサンプル素材が入っているので、必要に応じて差し替えてください。生成された MP4/WAV は常に output/ に保存されます。

開発サーバー

npm run dev

http://localhost:4000/docs で Swagger UI を確認できます。

Docker での起動

docker compose を利用するとローカルの Node.js を汚さずに起動できます。初回は config/example.stream-profile.docker.jsonconfig/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.jsonvoicevoxUrl もこのホスト名を参照するようデフォルトで設定しているため、Compose を使わない場合には実行環境に合わせて URL を変更してください。

開発用コンテナ (animation-streamer-dev)

  • ローカルの src/config/motions/output/ をボリュームマウントした ts-node 実行環境です。
  • 以下で起動できます。
    docker compose up animation-streamer-dev
  • ソースを編集すると即座に反映されます。tsconfig.json や依存関係を変えた場合は docker compose build animation-streamer-dev で再ビルドしてください。

公開イメージ (animation-streamer)

  • 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 で待ち受けます。PORTHOST を変更したい場合は config/stream-profile.jsonserver セクションを更新してください。

API 例

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/*(ストリーミング配信用)

ストリーミング配信 API

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/stop

OBS での視聴

OBS のメディアソースに rtmp://localhost:1935/live/main を指定してください。config/stream-profile.jsonrtmp.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 は予約語のため不可)。idrequests[].action に指定し、pathmotions/ からの相対パス(例: talk_idle.mp4dir_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)

speakLipSync アクションは、ベース動画に口画像をオーバーレイ合成し、音素レベルで同期したリップシンク動画を生成します。

2段階ワークフロー

  1. 事前処理(Python): ベース動画から口位置を検出しJSONファイルを出力
  2. 動画生成(TypeScript): 口位置JSONを読み込み、FFmpegでオーバーレイ合成

speak との違い

項目 speak(既存) speakLipSync
素材 モーション動画(mp4) ベースループ動画 + 口画像(png)× 6枚/emotion
口の動き 動画に含まれる(固定) 音声に合わせて口画像をオーバーレイ
同期精度 音声の長さのみ 音素レベルで同期
事前処理 不要 口位置検出が必要(Python)

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 スクリプトで出力した口位置 JSON
  • images: 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)。

API 例

# テキスト入力
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 エンジン

TTS エンジン タイムライン生成方式
VOICEVOX audio_query のモーラ情報(高精度)
Style-Bert-VITS2 MFCC 音声解析
直接音声使用 MFCC 音声解析

制限事項

  • lipSync 設定必須: プリセットに lipSync 配列がない場合はエラー
  • 口位置 JSON 必須: Python スクリプトで事前に生成が必要

音声入力 (STT)

speak アクションではテキストの代わりに音声ファイルを入力できます。

直接音声入力(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" } } }
    ]
  }'

音声→文字起こし→TTS(声質変換)

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 } } }
    ]
  }'

STT サーバーの設定

音声の文字起こし機能には 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"
  }
}

TTS エンジンの設定

audioProfile で使用する TTS エンジンを指定します。voices 配列には最低1件の設定が必要です。

VOICEVOX

{
  "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 の話者 ID
  • speedScale, pitchScale, intonationScale, volumeScale: 音声調整パラメータ
  • outputSamplingRate, outputStereo: 出力形式

Style-Bert-VITS2

{
  "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"

多数決で推奨基準が決定され、変換が必要なファイルのコマンドが自動生成されます。

Special Thanks

  • LipWI2VJs - 口の形の解析に参考にさせていただきました
  • MotionPNGTuber - LipSyncアクションの実装に参考にさせていただきました
  • wLipSync - MFCCプロファイルデータ提供

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors