プレイする
FastAPI & AI 投稿日: 2026年6月27日

AIお絵描きゲームの裏側:FastAPIとGemini APIで実現するリアルタイム線画解析

1. はじめに:なぜ「FastAPI × Supabase × Gemini API」なのか?

個人開発において、近年最も開発スピードとコスト効率を最大化できるアーキテクチャの一つが、Pythonの高速フレームワークである「FastAPI」BaaS(Backend as a Service)の「Supabase」、そして**Googleの高性能かつ低コストなLLM「Gemini API」**の組み合わせです。

「Doodle Fighter」は、ユーザーがWebブラウザ上で自由に描いた落書き(線画)をAIがプロファイリングし、性格モデル「Big Five」や戦闘力、固有の必殺技を持ったキャラクターを生成して他のプレイヤーと非同期バトルを行うゲームです。

このゲームの肝となるのが、「ユーザーが絵を描き終えてから、数秒以内にキャラクターが生成されてカード化される」というリアルタイム体験です。お絵描きゲームにおいて、診断に15秒や30秒も待たされてはユーザー体験(UX)は最悪になってしまいます。

そこで私たちは以下の理由からこの技術スタックを選定しました:

  • FastAPIによる超高速な非同期I/O処理: 非同期(async/await)ネイティブなFastAPIにより、並行リクエストが発生しても少ないメモリで高速に応答可能。
  • Gemini API (gemini-2.5-flash-lite) の圧倒的なスピードとコストパフォーマンス: 他社製LLMと比較して応答速度が極めて速く、さらにVisionモデルの呼び出しコストが非常に低く抑えられます。
  • Pydanticによる厳格な入出力チェック: 送信されてくるCanvasデータのメタデータ検証と、AIから返却されるJSONデータの構造定義をシームレスに結合できること。

本記事では、このリアルタイムAI線画解析システムの裏側と、実際のコードに基づいた実装のノウハウを公開します。

2. Canvasから送信されるデータの受け取りと前処理の工夫

フロントエンドのHTML5 Canvasで描かれたお絵描きデータは、一般的な画像ファイルのアップロード形式ではなく、Base64形式のデータURLとしてバックエンドに送信されます。

送信されるパラメータのスキーマ (Pydantic)

バックエンド(FastAPI)では、Pydanticを用いて以下のようにリクエストを受け取ります。

class CharacterCreate(BaseModel):
    player_id: str
    target_subject: str
    image_base64: str
    active_time_ms: int
    stroke_count: int

ただ画像を送るだけでなく、「描画にかかったミリ秒数(active_time_ms)」「線を引いた回数(stroke_count)」をメタデータとして同時に送信するのが大きな工夫です。これにより、AI単独では読み取れない「描き手の迷い(時間)」や「筆数の多さ」を性格パラメータ算出に反映させることができます。

バックエンド側での画像前処理と軽量化

送信されたBase64文字列は、AIに送信する前に適切な画像フォーマットにデコードし、リサイズ処理を施します。超高解像度のまま画像をAIに投げると、処理遅延(レイテンシの悪化)やトークン消費量の増加に繋がるため、最大 400x400 ピクセルへのサムネイル化をインメモリで行うのが実用上の最大のノウハウです。

import base64
import io
from PIL import Image

def preprocess_image(image_data: str) -> Image.Image:
    # Base64ヘッダー(data:image/png;base64,...)が含まれている場合は除去
    if "," in image_data:
        image_data = image_data.split(",")[1]
    
    img_bytes = base64.b64decode(image_data)
    img = Image.open(io.BytesIO(img_bytes))
    
    # AIが解析しやすいようRGBに変換
    if img.mode != "RGB":
        img = img.convert("RGB")
        
    # トークン節約と速度向上のため400x400にリサイズ
    img.thumbnail((400, 400))
    return img

3. Gemini API (Vision) へのプロンプト設計とJSON出力の制御

Gemini APIを使って線画から性格(Big Five)や能力を抽出するためには、プロンプトに「どのような役割で評価するのか」を明確に定義し、なおかつDB保存が可能な「構造化されたJSON」で返却させる必要があります。

プロンプト設計のポイント(YAMLでの分離管理)

Doodle Fighterでは、システムプロンプトの保守性を高めるため、Pythonコード内に直接記述するのではなく prompts.yaml という設定ファイルに分離して管理しています。

profiles:
  doodle_fighter_profiler:
    model: gemini-2.5-flash-lite
    temperature: 0.5
    system_prompt: |
      あなたは一切の妥協を排した「デジタルアート・マスター審査官」です。
      ユーザーが描いた絵の「客観的な完成度」と「視覚的な情報量」に基づき、キャラクターの強さと性格パラメータを算出してください。

      # 入力データ
      - お題: {target_subject}

      # 出力フォーマット(厳守)
      JSONのみを出力してください。マークダウン等の装飾は不要です。
      {{
        "special_move": "必殺技名",
        "combat_power": 0,
        "big_five": {{
          "openness": 0,
          "conscientiousness": 0,
          "extraversion": 0,
          "agreeableness": 0,
          "neuroticism": 0
        }},
        "comment": "講評テキスト"
      }}

FastAPIとGemini SDKによる非同期API連携のコア実装

Python用の新しい google-genai SDKを使用し、非同期(aio)でJSON出力を要請するコードの核心部分です。

from google import genai
from google.genai import types

_client = genai.Client(api_key=settings.gemini_api_key)

async def get_ai_response(variables: dict, img: Image.Image) -> dict:
    # プロンプトをYAMLからロードしてフォーマット
    formatted_prompt = system_prompt.format(**variables)
    contents = [formatted_prompt, img]

    # 非同期API呼び出しを実行
    response = await _client.aio.models.generate_content(
        model="gemini-2.5-flash-lite",
        contents=contents,
        config=types.GenerateContentConfig(
            temperature=0.5,
            # JSONモードを指定してAIに確実な構造化出力を指示
            response_mime_type="application/json",
        ),
    )
    return response.text

response_mime_type="application/json" を指定することで、Gemini APIはレスポンスとして妥当なJSON文字列を返却することが保証されます。

4. 開発・運用で直面した課題と解決策

課題①:AIが出力するJSONの文字列前後の「Markdownブロック化」

response_mime_type を指定していても、モデルやプロンプトの記述によっては希に ```json``` といったマークダウンの装飾記号がレスポンス文字列の前後に入り込み、json.loads() でパースエラー(HTTP 500)を引き起こすことがありました。

【解決策】: パース処理の手前に、文字列前後の不要なマークダウンタグを強硬にトリムするユーティリティ関数を導入しました。これにより、パースエラーによるサーバーダウン率をほぼ0%に抑え込んでいます。

def clean_json_text(text: str) -> str:
    text = text.strip()
    if text.startswith("```json"):
        text = text[7:]
    elif text.startswith("```"):
        text = text[3:]
    if text.endswith("```"):
        text = text[:-3]
    return text.strip()

課題②:意図しない不適切なイラストの送信(安全性フィルタの制御)

ユーザーが何を描くか自由であるゲームの性質上、ヘイト表現や成人向けコンテンツなどが投稿されるリスクが存在します。Gemini APIには強力なセーフティチェック(Safety Ratings)が内蔵されており、閾値を超えたコンテンツはAPI側で自動的にブロックされます。

【解決策】: APIレスポンスの「終了理由(finish_reason)」を監視し、FinishReason.SAFETY によって遮断された場合は、サーバーがクラッシュするのを防ぎ、ユーザーに「お絵描き内容を改めてもらう」エラーメッセージを丁寧に返却するように設計しました。

if response.candidates and response.candidates[0].finish_reason == types.FinishReason.SAFETY:
    raise Exception("不適切な内容が含まれている可能性があるため、解析が中断されました。別の絵を描いてみてください。")

5. まとめ

FastAPIとGemini APIをシームレスに連携させることで、「Webブラウザ上で描いた線画を即座にAIが分析し、Big Five性格特性にマッピングしてゲームキャラクターを生み出す」という動的なリアルタイムUXを、非常に軽量かつ低コストで実現できました。

特に、クライアント側でリサイズした最小限の画像を非同期API経由で投げる処理と、API側でJSONフォーマットを確約する出力制御は、AIを活用したツールやゲームの開発において極めて実用性の高いパターンです。

これから生成AIを用いたWebサービスの個人開発に挑戦される方は、ぜひこのスタックを検討してみてはいかがでしょうか。