[Next.js #40] MMD Clock AI Talk — LM Studio × VOICEVOX × MMDで「会話する時計アプリ」を作る

はじめに

今回は、これまで作ってきた MMD 時計アプリに 会話機能 を追加しました。

右下に会話 UI を追加し、入力欄からメッセージを送ると、MMD キャラクターが前中央へ歩いてきて、吹き出し と 音声 で返答します。 ローカル LLM には LM Studio、読み上げには VOICEVOX を使い、すべて Next.js アプリ内でつながるようにしました。

今回やりたかったのは、単なるチャット UI を置くことではありません。 メッセージ、音声、キャラクターの移動、吹き出し表示を一体化して、“時計の中に住んでいるキャラクター”として会話させること が目的でした。

結果として、かなり AI VTuber に近い体験が見えるところまで進みました。

前回の記事:

動画(YouTube):

動画(PC):

何を作ったか

今回追加したのは、主に次の流れです。

  • 右下に 会話ボタン と Voice ボタン を追加
  • 会話パネルからメッセージ送信
  • Next.js の API Route 経由で LM Studio に問い合わせ
  • 返答テキストを会話ログに表示
  • MMD キャラクターを 前中央へ歩かせる
  • 返答内容を 吹き出し で表示
  • VOICEVOX で返答を読み上げる

つまり、ただ返答文を画面に出すだけではなく、 キャラクターが前に出てきて、返事をして、実際に喋る ところまで統合しています。

これによって、UI の中に存在しているだけだったキャラが、 会話の主体としてちゃんと見えるようになりました。

なぜこの実装が面白いのか

普通のチャット UI は、テキストを入力して返答が返ってくるだけです。 それでも機能としては成立しますが、MMD キャラがいるアプリでは少し物足りません。

今回はそこを一歩進めて、

  • 質問したら
  • キャラが前に出てきて
  • 吹き出しを出して
  • 音声で返事する

という流れにしました。

この差はかなり大きいです。 単なる「チャット欄」ではなく、そこにいるキャラクターが応答している ように見えるからです。

MMD キャラを表示している以上、返答をテキストだけで完結させるよりも、

  • 移動
  • 向き
  • 吹き出し
  • 音声

までまとめて見せた方が圧倒的に面白いです。

時計アプリの中にキャラクターがいて、そのキャラがこちらへ歩いてきて返事をする。 この時点で、アプリの体験がかなり変わります。

実装の全体構成

会話 UI

まず右下に、会話ボタン と Voice ボタン を追加しました。

会話ボタンを押すとパネルが開き、

  • 入力欄
  • 送信ボタン
  • 会話ログ

が表示されます。

ここは最初から LM Studio や MMD 演出をつながず、まずは UI だけ先に実装 しました。

  • ボタン表示
  • パネル開閉
  • 入力できる
  • 送信イベントが走る
  • ログを表示できる

ここまでを先に確認してから、後で会話処理や音声をつないでいく形です。

この順番にしたことで、問題が起きた時に

  • UI の問題なのか
  • 会話処理の問題なのか
  • 音声や LLM 連携の問題なのか

を切り分けやすくなりました。

VOICEVOX 連携

読み上げは voicevox.js としてまとめました。

このファイルでは主に、

  • speaker 設定
  • クレジット表記
  • AudioQuery パラメータ
  • ON / OFF 状態
  • speakText() による簡易呼び出し

を管理しています。

また、ブラウザの自動再生制限があるため、 右下に Voice ボタン を用意して、音声の ON / OFF を明示的に切り替える形にしました。

これによって、

  • 音声が有効なのか
  • まだ解除されていないのか
  • 接続が通っているのか

が分かりやすくなりました。

最初は「音が出るかどうか分からない」という状態でしたが、 ボタン経由で有効化する形にしたことで、かなり扱いやすくなりました。

LM Studio 連携と CORS

ローカル LLM は LM Studio を使用しました。

最初はブラウザから直接 127.0.0.1:1234fetch() しようとしましたが、これは CORS で失敗しました。 VOICEVOX と同じで、ローカルに立っているサーバーでもポートが違えば別オリジン扱いになるため、そのままでは通りません。

そこで今回は、Next.js の API Route を使って

app/api/chat/route.js

を追加し、ここを LM Studio への中継 API にしました。

流れとしてはこうです。

  • フロントの chat.js から /api/chat に送信
  • route.js が LM Studio の OpenAI 互換 API を呼ぶ
  • 返答テキストだけをフロントへ返す

この構成にしたことで、フロントは同一オリジンの /api/chat にしかアクセスしないので、ブラウザ側の CORS 問題を回避できました。

ここは今回の実装の中でも、かなり重要なポイントでした。

会話処理の分離

ui.js はすでにかなり大きくなっていたので、会話処理そのものは chat.js に分けました。

役割としては、

  • ui.js → 見た目、入力、パネル開閉、ログ描画
  • chat.js → 会話の送信、LLM 呼び出し、返答管理
  • voicevox.js → 音声読み上げ
  • route.js → LM Studio 中継

という構成です。

まだ完全に整理し切れているわけではありませんが、 少なくとも UI と会話ロジックを分ける方向 には持っていけました。

この分離をしておいたことで、あとから

  • ダミー返答
  • LM Studio 返答
  • 口調制御
  • 時刻回答
  • キャラ演出

を足す時にかなり楽になりました。

MMD キャラクターの移動

今回の見せ場はここです。

もともと歩行ターゲット制御の基盤はあったので、前中央アンカーを利用して、 メッセージ送信時にキャラクターを前中央へ歩かせる 処理を追加しました。

これによって、会話が始まるとキャラが奥から前へ出てきます。

これだけでもだいぶ印象が変わります。 画面のどこかに立っているだけだったキャラが、会話に反応してこちらへ寄ってくるので、 「応答主体」が明確になります。

今回やりたかったのはまさにここで、 メッセージログと 3D キャラクターを分離させず、会話とキャラの行動を接続する ことでした。

吹き出し表示

移動だけではまだ足りないので、次に 吹き出し表示 を足しました。

LLM が返したテキストをキャラ頭上に出し、 会話ログだけでなく、3D 空間の中でも返答が見えるようにしています。

これによって、

  • パネル内ログ
  • キャラ頭上の吹き出し
  • 音声読み上げ

が同時に成立し、返答内容がキャラクター自身のものとして見えやすくなりました。

この時点でかなり「AI VTuber感」が出てきます。

到着を待ってから話す処理

最初は、送信した瞬間に歩き出して、返答が来たらそのまま話す形でもよいかと考えていました。 ただ、やはり 前に来てから話した方が自然 です。

そこで、前中央アンカーとの距離を見て、 一定距離まで近づいたら吹き出しと音声を出すようにしました。

流れとしては次のようになります。

  1. ユーザーがメッセージ送信
  2. キャラが前中央へ歩く
  3. 裏で LLM 返答を取得
  4. 到着判定を待つ
  5. 到着後、吹き出し表示
  6. VOICEVOX で返答を読み上げる

これによって、会話体験がかなり自然になりました。

「歩いてきてから返事する」というだけで、 メッセージとキャラクターがかなり強く結びつきます。

実際にできあがったもの

ここまで実装した結果、

  • 時計アプリ
  • MMD キャラクター
  • 会話 UI
  • ローカル LLM
  • ローカル音声合成
  • 移動演出
  • 吹き出し表示

が、ひとつの流れとして動くようになりました。

しかも今回はすべてローカルで動いています。

  • LM Studio
  • VOICEVOX
  • Next.js
  • MMD 演出

を全部ローカルでつなげて、 キャラクターに話しかけると前に出てきて返事する時計アプリ が成立しました。

ここまで来ると、もうただの時計アプリではありません。

ハマった点

今回ハマったところは主に次の3つです。

VOICEVOX の CORS

最初は 192.168.x.x 側で開いていたため、VOICEVOX Engine へのアクセスが通りませんでした。 127.0.0.1 で開くようにして解決しました。

LM Studio の直叩き

ブラウザから直接 LM Studio を叩こうとして CORS で止まりました。 Developer Mode を ON にするだけでは解決せず、 最終的に Next.js API Route 経由 に切り替えて通しました。

ui.js の肥大化

会話 UI 追加前から ui.js はかなり大きくなっており、今回さらに会話パネル、Voice ボタン、ログ表示などが追加されたため、リファクタ対象としてかなり意識するようになりました。

現時点では急ぎだったので全面分割していませんが、 今後は ui-chat.js などに抜いていく余地があります。

今回の実装で得たもの

今回いちばん大きかったのは、 「会話できるキャラクター」ではなく、「会話の主体として存在しているキャラクター」 に近づいたことです。

ログだけではなく、

  • 前に出てくる
  • 吹き出しを出す
  • 音声で返す

という流れが入ったことで、会話がかなり立体的になりました。

これはかなり大きいです。

単なる API 接続や音声再生の確認ではなく、 キャラクターと UI と LLM を一つの体験に統合した ところまで来ました。

今後やりたいこと

今回の実装で土台はかなりできたので、次は質を上げる方向に進めそうです。

たとえば次にやりたいのはこのあたりです。

  • system prompt を調整してキャラ性を強める
  • 現在時刻や曜日をコンテキストとして渡す
  • 時刻回答だけは専用処理にする
  • 表情モーフやモーション切替を会話と連動させる
  • 会話終了後に元の位置へ戻る
  • 配信コメントを入力元にして AI VTuber 化する

今回で、 MMD × ローカル LLM × VOICEVOX × Next.js の組み合わせが十分成立することは確認できました。

ここから先は、返答の質だけではなく、口調、表情、移動、間の取り方まで含めて作り込んでいく段階になりそうです。

まとめ

今回は、MMD 時計アプリに会話機能を追加し、 LM Studio のローカル LLM と VOICEVOX を統合して、 キャラクターが前へ歩いてきて返答する AI 会話機能 を実装しました。

技術的には、

  • 会話 UI
  • ローカル音声
  • Next.js API Route
  • ローカル LLM
  • MMD 移動演出
  • 吹き出し表示

を接続した回でしたが、完成した体験としてはかなり面白いものになりました。

質問するとキャラクターがこちらへ歩いてきて、吹き出しを出しながら返事をする。 この時点で、かなり AI VTuber 的な体験 になっています。

次は、時刻や天気のような時計アプリ固有の文脈を返答に混ぜつつ、 さらに自然な会話演出へ寄せていきたいと思います。