テクノロジー

Asset Studioはどう実装したのか

AAnonymous
10分で読めます

はじめに

前回の ゲーム制作計画 では、どのジャンルをどの順番で作っているのかを説明しました。あの記事で Asset Studio はほんの少しだけ登場しました。Story Quiz と Mystery のキャラクターや背景アセットをまとめて管理するツール、という程度でした。

今回は、その一文を実装ベースでほどいてみます。Asset Studio は単に画像を生成する画面ではありません。現在の Omnilude では、この画面を中心にアセットを集め、プロンプトを整え、生成ジョブを実行し、結果を確認し、最後にライブラリへ登録しています。フロントエンドのバックオフィスが作業台になり、game-service がアセットドメインを持ち、ai-service が生成実行系を担っています。

なぜ Asset Studio を分けて作ったのか

実際にゲームを作り始めると、アセットは思っている以上に早く散らばります。Story Quiz にはキャラクター、背景、ブロック用の画像があり、Mystery にはシナリオキャラクター、場所、ノート用の画像があります。同じ人が同じ世界観を作っていても、プロンプト、スタイルプリセット、生成結果、承認状態、実際の利用状況を別々の画面で管理すると、すぐにコントロールが難しくなります。

そのため Asset Studio は、最初からギャラリーよりも作業場に近いものとして設計しました。重要なのは、このツールが特定のジャンル専用ではないことです。Story Quiz ですでに作ったアセットと Mystery のドラフトから引いてきたアセットを同じやり方で扱い、生成結果をレビュー可能なライブラリとして残し、後から別のドメインで使われているかどうかまで追跡します。この視点がないと、AI 生成は便利でも運用アセットとしては残りません。

バックオフィス画面はどう構成されているのか

フロントエンドのバックオフィスには Asset Studio 専用のルートがあります。背景とキャラクターは別々の入口から開きますが、内部では同じ作業台コンポーネントを共有しています。一覧画面はツリービューとフラットビューの両方に対応しており、最近編集した項目も一緒に表示します。つまり、ディレクトリ中心で整理することも、最近の作業から戻ることもできます。

詳細画面に入ると、構造はさらに明確になります。この画面は、実質的にはひとつのアセット制作コンソールです。上部では名前と承認状態を管理し、本体ではインポートソース、プロンプト、生成、メディア、履歴をタブ単位で扱います。実装を見ると、インポートソースの検索状態と生成済みメディアのアクション状態をページレベルで保持するようになっています。タブを行き来しても文脈を失わないようにするためです。

ここには重要な分岐がひとつあります。アセット一覧の取得、ディレクトリ更新、生成結果の登録といったアセット管理 API は、すべて /api/asset-studio/* を通って game-service に向かいます。一方で、実際の生成リクエストは Asset Studio API を通らず、/api/media-generation/generate から ai-service に送られます。この分離は意図的なものです。アセット管理と生成実行は同じ画面にありますが、同じ責務ではありません。

実際の生成フローはどう流れるのか

ユーザー体験自体はかなり単純です。アセットを開き、既存アセットや Story Quiz や Mystery のソースを取り込み、プロンプトを調整して生成ボタンを押します。ただし内部の流れは次のように分かれています。

  1. バックオフィスの詳細画面で現在の assetItem.generationConfig を読み込みます。
  2. 生成リクエストは /api/media-generation/generate に送られ、フロントエンドはすぐに jobId を受け取ります。
  3. フロントエンドはその jobId で backbone SSE ストリームを購読し、進捗を受け取ります。
  4. ai-service は実際のプロバイダーで結果を生成し、storage にアップロードします。
  5. フロントエンドは SSE から outputUrlsourceUrl を受け取り、プレビューを表示します。
  6. 結果が問題なければ、その時点で /api/asset-studio/asset-items/{id}/media へ登録します。

この最後の段階が重要です。生成が終わった瞬間に Asset Studio のライブラリへ保存するわけではありません。生成結果はまずレビュー対象として扱われます。運用アセットになるのは、ユーザーが登録アクションを押したときです。実装上もその流れは明確です。生成フックが SSE からプレビュー状態を作り、別のアクションが createGeneratedMediaApi を呼んでライブラリへ取り込みます。

この構造によって、Asset Studio は単なるプロンプト入力画面ではなく、レビューを含んだ制作パイプラインになります。失敗した結果、判断が難しい結果、パイプラインの後処理結果をいったん仮の状態で持ち、必要なものだけを登録できるからです。

game-service は何を担当しているのか

game-service 側の Asset Studio は単なる CRUD の束ではありません。実際にはアセットライブラリの所有者にかなり近い存在です。コントローラのレベルでも、ディレクトリ、アイテム、インポート、export/import portability、generated media、winner 指定、reorder、bulk move、import source 取得などがひとつのドメインとしてまとまっています。

このサービスが特に重要なのは三つの理由があります。

1. 既存のゲームドメインからソースを引いてくる

AssetStudioImportService は Mystery の DraftLocationDraftCharacter、Story Quiz の StoryQuizBackgroundAssetStoryCharacter を読み込み、インポートソースカードに変換します。このとき、単に名前だけを持ってくるのではなく、prompt、provider、stylePreset、メタ情報までまとめて扱います。つまり Asset Studio は、すべてのアセットをゼロから作る場所ではなく、既存のゲームデータからアセット制作を引き継ぐ場所になっています。

2. 生成結果を運用アセットへ整える

AssetGeneratedMediaService は生成結果をライブラリレコードとして保存し、その中から winner を指定できるようにします。画像タイプの winner が変わると、アイテムのサムネイルも一緒に更新されます。逆に、すでに使われているアセットの代表メディアは簡単には削除できないようにしています。生成結果を集めることと、運用アセットとして採用することを分けているわけです。

3. 実際に使われているかどうかを追跡する

AssetReferenceSyncService は Story Quiz と Mystery の中で URL が実際に使われているかを確認し、isReferenced を同期します。Story Quiz 側ではカバー、カード、キャラクターの基本画像、感情画像、背景、ブロックメディアを見ます。Mystery 側ではシナリオのサムネイル、キャラクターのアバター、ノート、オブジェクト画像まで確認します。だから Asset Studio は見栄えのよいアセット保管庫ではなく、ライブコンテンツにつながったアセット管理ツールになります。

ai-service は何を担当しているのか

ai-service 側の責務はさらに明確です。このサービスはアセットライブラリを知りません。代わりに生成実行を担当します。バックオフィスから送られてきた generationConfig を解釈し、どのプロバイダーとワークフローを使うかを決め、非同期ジョブとして実行し、SSE で進捗を流します。

実装を見ると、MediaGenerationService はまずアカウント単位の同時実行スロットを確認します。その次に GenerationConfigParser が設定を解析します。ここでの優先順位はかなり重要です。generationConfig.media[mediaType].providers[providerCode] が最優先で、次に default、最後にルートレベルが fallback になります。この構造によって、ひとつのアセットがメディアタイプごと、プロバイダーごとに異なる設定を持てます。

解析が終わっても、ai-service はその場で生成を始めません。DTE Job のキューに積みます。フロントエンドが受け取る jobId は、その後の SSE 購読キーとしてそのまま使われます。つまりバックオフィスは長いリクエストを握ったまま待つのではなく、job ID を軸に非同期の生成過程を追跡します。

ComfyUI 連携が重要な理由

現在の実装で Asset Studio の面白い点は ComfyUI 連携です。ComfyUIProvider はコードの中にワークフローを埋め込んでいません。ランタイム設定は provider 設定から読み、実行ワークフローは DB の workflow レコードから読み込みます。そして prompt-to-image だけでなく、media-to-media パイプラインもサポートしています。

ここが重要なのは、Asset Studio の生成設定が単なる prompt 文字列ではないからです。ユーザーは workflowIdworkflowPipelineseedwidthheight、入力画像 URL といったランタイムオーバーライドを一緒に送れます。実際の実装でも、パイプラインワークフローを選ぶと、既存の generated media を入力にして後処理や変形をかけられるようになっています。

結果として、AI 生成画面は汎用的なプロンプト入力欄ではなく、データ駆動のワークフロー実行機になります。同じキャラクターでも別の provider、別の workflow、別の入力画像で再実行でき、その結果をレビューしたうえでライブラリに積み上げていけます。

将来的には ComfyUI 自体をもう少し詳しく掘り下げる記事があってもよさそうです。

なぜ game-serviceai-service を分けたのか

これは単なる MSA の好みの話ではありません。アセットドメインと生成実行系は、壊れ方も、ライフサイクルも、運用者から見た視点も違います。

game-service はディレクトリ構造、承認状態、実際の利用状況、portability、history のように長く残る情報を扱います。対して ai-service は、どのプロバイダーが生きているか、どの workflow を選ぶべきか、今いくつのジョブを動かせるか、生成がどこまで進んでいるかを扱います。これらをひとつにまとめると、しばらくは便利でも、境界がすぐに曖昧になります。

今の構造では Asset Studio がその二つをつないでいます。バックオフィスはひとつの画面で作業しますが、保存と実行は分けて扱います。この分離によって、Story Quiz や Mystery 以外のジャンルが増えても、アセットライブラリと生成インフラを別々に進化させていけます。

まとめ

前回の記事では、Asset Studio は通り過ぎる一文にすぎませんでした。ですが実装を追っていくと、これは単なる画像生成機能ひとつを意味していません。Omnilude における Asset Studio は、ゲームアセットを集め、既存ドメインからソースを引き込み、AI 生成ジョブを実行し、結果をレビューし、そのアセットが実際に使われているかまで追跡する制作システムです。

だから私は Asset Studio をゲーム開発の脇役の画面とは見ていません。むしろ Story Quiz と Mystery を含む制作パイプライン全体が交わる接点に近いものです。次回は、このアセットシステムが実際の出版パイプラインや特定ジャンルの制作フローの中でどう使われているのか、もう少し絞って説明してみようと思います。

レビューアーとして読み返してみても、この文章は少し情報量が多く、暗号のように感じられる内容になってしまいました。

画像添付に対応するようブログを更新し、既存の記事も含めて全体の読みやすさを高める作業を近いうちに進めるつもりです。

それでも、ここまで書いた記事なので、いったん公開しようと思います。