👗

【誰でもコピペで 30 分】Google Virtual Try-On を使った試着アプリを Cloud Run で動かすハンズオン!

に公開

この記事で紹介するコードは生成 AI(GPT-5)が作成したものですが、私、ラリオスが責任をもって動作確認をしています。記事の構成にも生成 AI を活用していますが、より分かりやすくなるよう解説やキャプチャを追加し、全面的に手を加えて仕上げました。この記事を通して、エンジニアでない方でも「生成 AI を使えば簡単なアプリを作れる」と実感していただけたらうれしいです。

0. はじめに

みなさん、こんにちは!ラリオスです。

今年の Google I/O で紹介された Virtual Try-On をご存じでしょうか。

https://x.com/emoiralios/status/1924894398598545691
https://note.com/ralios/n/nf9dc8ad3b4c9
https://blog.google/intl/ja-jp/google-io-2025/

最近、この機能を呼び出せる API がプレビュー公開されたので、私も実際に触ってみました。人物画像と服の画像を渡すと、Vertex AI が合成して「試着後のイメージ」を返してくれます。

今回、私は Cloud RunFlask を使って、ブラウザから画像をアップロードするだけで着せ替えを試せる Web アプリを作ってみました。

下のようなシンプルな画面です(Cloud Run 上で実際に動かしたもの)。


Vertex AI については、この記事の中で紹介しているので、興味のある方は読んでみてくださいね!

https://note.com/ralios/n/nde2130d758ad#fadb162c-2b20-49fc-9f99-60402f06bf35

Cloud Run は、コンテナ化されたアプリケーションを実行するための、フルマネージドなサーバーレス プラットフォームです。これにより、サーバーの構築やスケーリングといったインフラ運用から開発者は解放され、本来の目的であるアプリケーションの価値創造に集中できます。(と言っても難しいですよね。Cloud Run は、コンテナという技術を使ったアプリケーションを面倒な管理や手間をかけずに公開できるサービスと理解しておくとよいでしょう!)

https://cloud.google.com/run?hl=ja

コンテナは、アプリケーションのコードと、その実行に必要なもの(依存性)をひとまとめにする技術のことです。

https://cloud.google.com/learn/what-are-containers?hl=ja


そこで、ぜひみなさんにも試してほしいと思い、ハンズオンの記事を書きました。今回のハンズオンは、私が実際にうまくいった手順だけに絞っています。約 30 〜 60 分 で、みなさんのプロジェクトでも同じアプリを立ち上げられるようになります。なお、非エンジニアの方でも迷わないように、コピペでそのまま動くコードCloud Shell での操作 に統一しています。

それでは、一緒に試していきましょう。


対象読者

  • IT 初心者の方
  • Google Cloud をこれから触ってみたい方
  • コピペで動くところから学びたい方

この記事で作るもの

  • 冒頭に紹介した「着せ替えアプリ」を作成します。
  • 人物画像と服の画像をアップロードし、「着せ替えを実行」を押すと、数十秒で結果の JPEG が表示されます。
  • 画像が安全基準に合わない場合は、理由の要約メッセージを画面に表示します。
  • 画像や個人情報はサーバーに保存しません。送信された画像はリクエストの処理中だけ使い、応答後に破棄します(学習にも使いません)。
  • 使用技術は Cloud Run(Flask) と Vertex AI の Virtual Try-On です。構成は次のとおりです。
  ブラウザ → Cloud Run(Flask) → Vertex AI(Virtual Try-On) → 画像を返す

作業時間の目安|30 〜 60 分(初めてでもここを目標に進めます)
費用の目安|検証レベルなら数十円程度を想定しています(無料トライアルに登録された方は、付与されたクレジットが消費されます)
※ Cloud Run、Cloud Build、Artifact Registry、Vertex AI の利用に応じて課金が発生します。

このあと進める順番

  1. 事前準備の確認(Google アカウント/無料トライアル)
  2. プロジェクト作成とリージョンの決定
  3. 必要な API を有効化(Vertex AI、Cloud Run、Cloud Build、Artifact Registry)
  4. Cloud Shell を開く
  5. コードを一括作成(app.pyindex.htmlrequirements.txtDockerfile
  6. コンテナをビルドして Artifact Registry にプッシュ
  7. Cloud Run にデプロイ(環境変数の設定を含む)
  8. 動作確認(アップロードと生成、失敗時のメッセージの見方)
  9. 片付け(課金が気になる場合の停止方法)

画像選びのコツ(とても大事)

Virtual Try-On には安全性フィルタがあります。次のような画像は失敗しやすいので避けると安定します。

  • 過度な露出、著名人、はっきりしたブランド ロゴ、極端な加工や合成感の強い画像
  • 服の切り抜きが粗いもの、解像度が極端に低いもの
  • 人物がほぼ写っていない、または顔が隠れすぎているもの

迷ったら、明るい場所で正面を向いた人物写真+服の正面写真を選んでください。うまくいかない時は画像を変えて試すのが近道です。

実際の失敗例


安全性フィルタにはしきい値があるため、同じ画像でも生成に成功したり失敗したりすることがあります。

この記事で扱わないこと

  • 高度な背景差し替えや細かいパラメータの調整
  • 大量アクセスを捌く構成
    まずは「確実に体験できる最小構成」に絞ります。慣れてきたら拡張しましょう。

1. 事前準備とプロジェクト作成

ここでは、はじめて Google Cloud を触る想定で、無料トライアルの開始からプロジェクト作成までを一気に進めます。スクショ画面の文言は少し変わることがあります。

1-1. 無料トライアルの開始(事前準備)

  1. Google アカウントでサインイン
    ブラウザで Google Cloud コンソールにアクセスし、普段使っている Google アカウントでサインインします。

https://cloud.google.com/cloud-console?hl=ja

個人の Google アカウントで登録を進めます。



2. 無料トライアルを開始
画面上の案内に沿って無料トライアルを開始します。

  • 90 日間で最大 300 ドル分のクレジットが付与されます。
  • 本人確認のためクレジットカード登録が必要ですが、90 日間 300 ドル以上の利用がなければ、自動課金は発生しません。


    同意して続行してください。



新しいお支払プロファイルを作成します。



プロファイルの種類は「個人」を選択しましょう。



登録が完了するとコンソールの初期画面が開きます。



フルアカウントを有効化して、クレジットをゲットしましょう!

よくあるつまずき

  • 複数の Google アカウントを使い分けている場合、意図しないアカウントで進めてエラーになることがあります。必要ならシークレットウィンドウで作業してください。
  • 無料トライアル登録が未完了のままだと、後続の有効化やデプロイで「課金が設定されていません」と表示されます。

1-2. プロジェクトを新規作成

  1. プロジェクトセレクタを開く
    コンソール左上部のプロジェクト名「My First Project」をクリックし、[新しいプロジェクト]を選びます。

Google Cloud における「プロジェクト」は、請求・権限・API 設定、そしてリソース(私たちが利用するサービス)をひとまとめに管理する単位(箱のようなもの)です。



2. 基本情報を入力

  • 任意のプロジェクト名を入力します。
  • プロジェクト名(例)virtual-tryon-handson
  • 組織が無い場合、ドメインは「組織なし」で問題ありません。
    [作成]を押すと数秒で完了します。


  1. プロジェクトの一覧を開く
    作成が終わったら、もう一度プロジェクトセレクタをクリックします。


  2. プロジェクト ID をメモ
    以降のコマンドで使うため、プロジェクト ID(英数字とハイフンの値)を控えます。(プロジェクト ID は自動的に割り当てられます)
    例)virtual-tryon-handson-123456


1-3. Cloud Shell を開いて初期設定

Cloud Shell は、ブラウザだけで使える Google Cloud の作業用ターミナルです。自分の PC に何もインストールしなくても、コマンドと呼ばれる命令文を打つだけで、Google Cloud の操作(サービスを起動したり停止するなど)が実行できます。このハンズオンは Cloud Shell を使う前提なので、黒い画面にコピペするだけで最後まで進められます。

  • 準備不要|インストールなし・ブラウザからすぐ使える
  • 権限そのまま|あなたの Google アカウント & 選択中のプロジェクトで動作
  • ファイルは保持|ホーム(~)の中身は次回も使えます(例:~/virtual-tryon)
  • コツ|しばらく操作しないと停止することがあります。再開後は cd ~/virtual-tryon で戻れば OK
  1. Cloud Shell を起動
    コンソール右上のターミナルアイコンを押して Cloud Shell を開きます(初回は数十秒かかります)。



「続行」を押してください。



続けて「承認」を押してください。



以下のような黒い画面が出てきたら準備は完了です!

コマンドの実行方法

手順の黒い枠の右上にカーソルを合わせるとコピーボタンが出てきます。

Windows ならctrl + V、Mac なら command + Vのショートカットで貼り付けできます。


  1. プロジェクトを設定
    下のコマンドを Cloud Shell にそのままコピペして実行します。<YOUR_PROJECT_ID> を「手順 1-2.の 4」でメモした値に置き換えてください。(Cloud Shell の操作に慣れていない方は、メモ帳などで先に<YOUR_PROJECT_ID>を置き換えてから Cloud Shell に貼り付けるとよいでしょう)
gcloud config set project <YOUR_PROJECT_ID>
gcloud config list project

うまく実行ができたら以下のような結果が表示されます



3. アカウントの確認(必要に応じて)
実行しているアカウントが正しく選択されているか迷ったら次を実行して、想定のアカウントか確認できます。

gcloud auth list

ここまでのチェック

  • Cloud Shell で gcloud config list project の出力が、作成したプロジェクト ID になっている

2. サービス有効化と最小ファイル作成

ここからは Cloud Shell だけ で進めます。まず必要な API を有効化し、動く最小構成のアプリ(Flaskgoogle-genai のみ、着せ替えだけ)を作ります。

Flask って何?
Flask は、Python で動く軽量な Web フレームワークです。最小限のコードで「URL に来たリクエストを受け取り、処理して、レスポンスを返す」仕組みを作れます。ひとつのファイルにルート(/ や /tryon のような URL)と処理を書くスタイルなので、学習コストが低く、今回のような小さなアプリと相性がよいです。このハンズオンでは、Flask が次の役割を担います。

  • index.html を配信して、ブラウザの画面を表示する
  • フォームから送られた画像を受け取り、Vertex AI の Virtual Try-On API に渡す
  • 返ってきた画像バイトをそのままブラウザへ返す(サーバー保存はしません)

また、Flask はコンテナ化との相性がよく、Cloud Run でもそのまま動きます。代替として FastAPI や Django もありますが、今回は「非エンジニアでも 30〜60 分で体験できる最小構成」を重視して Flask を選んでいます。用途に応じて他のフレームワークに置き換えることもできますが、まずは Flask で Web アプリの基本の流れをつかむのがおすすめです。

2-1. 必要な API を有効化

Cloud Shell にそのまま貼り付けて実行してください。

# 念のため現在のプロジェクトを確認
gcloud config list project

# 必要な API を有効化(数分かかることがあります)
gcloud services enable \
  run.googleapis.com \
  artifactregistry.googleapis.com \
  cloudbuild.googleapis.com \
  aiplatform.googleapis.com

API が有効化されるとこのような結果が返ってくるはずです。


2-2. 作業ディレクトリを作成

作業ディレクトリとは?
作業ディレクトリ(=作業フォルダ)は、今回のファイルを置いて作業する場所のことです。Cloud Shell では、いま自分がいる場所(カレントディレクトリ)を指します。この記事では ~/virtual-tryon を作業ディレクトリとして使います。ここに app.pyindex.htmlrequirements.txtDockerfile を作り、以降のコマンドもここで実行します。

mkdir -p ~/virtual-tryon && cd ~/virtual-tryon

青色の文字で、作業ディレクトリが表示されるはずです。このあとの作業は、必ずこの青色の文字(この場合は、/virtual-tryon)が表示された状態でコマンドをコピペしてください。

2-3 ファイルをまとめて作る(コピペ一発)

Cloud Shell のターミナルに 下のスクリプトを丸ごと貼り付けて Enter します。

このコマンドを実行すると、アプリの設計図とも言える 4 つのファイル作成(app.pyindex.htmlrequirements.txtDockerfile)を一気に終わらせます。

# ===== 2章:ファイルをまとめて作る(コピペ一発)=====

# 作業ディレクトリ作成&移動
mkdir -p ~/virtual-tryon
cd ~/virtual-tryon

# app.py(VTO_MODEL は環境変数があれば優先、なければデフォルト値)
cat > app.py <<'PY'
import io
import os
from flask import Flask, request, send_from_directory, jsonify
from PIL import Image as PILImage

import google.genai as genai
from google.genai import types
from google.cloud import storage
from werkzeug.exceptions import RequestEntityTooLarge

app = Flask(__name__)
app.config["MAX_CONTENT_LENGTH"] = 25 * 1024 * 1024  # 25MB

@app.after_request
def add_headers(resp):
    resp.headers["Cache-Control"] = "no-store, max-age=0"
    resp.headers["Pragma"] = "no-cache"
    resp.headers["Expires"] = "0"
    return resp

PROJECT_ID = os.environ.get("PROJECT_ID")
LOCATION = os.environ.get("LOCATION", "us-central1")

# 環境変数 VTO_MODEL があればそれを、無ければデフォルトを使う
VTO_MODEL = os.environ.get("VTO_MODEL", "virtual-try-on-preview-08-04")
print(f"[boot] Using VTO_MODEL={VTO_MODEL}")

def _client() -> genai.Client:
    if not PROJECT_ID:
        raise RuntimeError("環境変数 PROJECT_ID が未設定です")
    return genai.Client(vertexai=True, project=PROJECT_ID, location=LOCATION)

def _ensure_jpeg_bytes(src_bytes: bytes) -> bytes:
    with PILImage.open(io.BytesIO(src_bytes)) as im:
        if im.mode not in ("RGB", "L"):
            im = im.convert("RGB")
        out = io.BytesIO()
        im.save(out, format="JPEG", quality=90)
        return out.getvalue()

def _image_from_upload(file_storage) -> types.Image:
    if not file_storage:
        raise ValueError("ファイルが未指定です")
    raw = file_storage.read()
    if not raw:
        raise ValueError("ファイルが空です")
    jpg = _ensure_jpeg_bytes(raw)
    return types.Image(image_bytes=jpg, mime_type="image/jpeg")

def _bytes_from_genai_image(img: types.Image) -> bytes | None:
    if getattr(img, "image_bytes", None):
        return img.image_bytes
    gcs_uri = getattr(img, "gcs_uri", None) or getattr(img, "uri", None)
    if gcs_uri and str(gcs_uri).startswith("gs://"):
        bucket_name, blob_name = gcs_uri[5:].split("/", 1)
        blob = storage.Client().bucket(bucket_name).blob(blob_name)
        return blob.download_as_bytes()
    return None

@app.route("/")
def index():
    return send_from_directory(os.getcwd(), "index.html")

@app.route("/tryon", methods=["POST"])
def tryon():
    try:
        person_file = request.files.get("person_image")
        garment_file = request.files.get("garment_image")
        if not person_file or not garment_file:
            return jsonify({"error": "人物画像と衣服画像の両方を選んでください"}), 400

        client = _client()
        person_img = _image_from_upload(person_file)
        product_img = _image_from_upload(garment_file)

        vto_source = types.RecontextImageSource(
            person_image=person_img,
            product_images=[types.ProductImage(product_image=product_img)],
        )
        vto_config = types.RecontextImageConfig(
            number_of_images=1,
            output_mime_type="image/jpeg",
        )
        resp = client.models.recontext_image(
            model=VTO_MODEL,
            source=vto_source,
            config=vto_config,
        )

        if not getattr(resp, "generated_images", None):
            return jsonify({"error": "生成に失敗しました(generated_images なし)"}), 500

        out_img = resp.generated_images[0].image
        out_bytes = _bytes_from_genai_image(out_img)
        if not out_bytes:
            hint = "ヒント: 露出や著名人・ロゴ類・過度な加工を避け、人物と衣服がはっきり写る画像で再試行してください。"
            return jsonify({"error": f"VTO出力の画像バイト列が空です。{hint}"}), 500

        return (out_bytes, 200, {"Content-Type": "image/jpeg"})
    except Exception as e:
        return jsonify({"error": f"{type(e).__name__}: {str(e)}"}), 500

@app.route("/healthz")
def healthz():
    return "ok"

@app.errorhandler(RequestEntityTooLarge)
def handle_413(e):
    return jsonify({"error": "画像が大きすぎます(25MBまで対応)"}), 413

PY

# index.html(テンプレートリテラルの ${...} はエスケープしない)
cat > index.html <<'HTML'
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8"/>
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
  <title>Vertex AI Virtual Try-On</title>
  <script src="https://cdn.tailwindcss.com"></script>
  <style>
    body { font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, sans-serif; }
    .preview-box{width:100%;height:320px;background:#f3f4f6;border:2px dashed #d1d5db;display:flex;align-items:center;justify-content:center;overflow:hidden}
    .preview-box img{max-width:100%;max-height:100%;object-fit:contain}
    .loader{border:8px solid #f3f3f3;border-top:8px solid #6366f1;border-radius:50%;width:56px;height:56px;animation:spin 1.2s linear infinite}
    @keyframes spin{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}
  </style>
</head>
<body class="bg-gray-50 text-gray-800">
  <div class="container mx-auto p-4 md:p-8 max-w-5xl">
    <header class="text-center mb-8">
      <h1 class="text-3xl md:text-4xl font-bold text-gray-900">Vertex AI Virtual Try-On</h1>
      <p class="text-gray-600 mt-2">人物と衣服の画像をアップロードして、着せ替えを試そう。</p>
    </header>

    <main class="bg-white p-6 md:p-8 rounded-2xl shadow-lg">
      <form id="tryon-form">
        <div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
          <div>
            <label class="block text-lg font-semibold mb-2 text-gray-700">1. 人物画像</label>
            <div id="person-preview" class="preview-box rounded-lg mb-3"><span class="text-gray-500">プレビュー</span></div>
            <label class="inline-flex items-center px-4 py-2 rounded-full bg-gradient-to-r from-indigo-500 to-blue-500 text-white shadow hover:shadow-lg cursor-pointer">
              <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6H17a3 3 0 010 6h-1m-4 5V10m0 0l-2 2m2-2l2 2"/></svg>
              <span>ファイルを選択</span>
              <input type="file" id="person-image" name="person_image" accept="image/*" class="hidden" required>
            </label>
            <span id="person-name" class="ml-2 text-sm text-gray-500"></span>
          </div>

          <div>
            <label class="block text-lg font-semibold mb-2 text-gray-700">2. 衣服画像</label>
            <div id="garment-preview" class="preview-box rounded-lg mb-3"><span class="text-gray-500">プレビュー</span></div>
            <label class="inline-flex items-center px-4 py-2 rounded-full bg-gradient-to-r from-indigo-500 to-blue-500 text-white shadow hover:shadow-lg cursor-pointer">
              <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6H17a3 3 0 010 6h-1m-4 5V10m0 0l-2 2m2-2l2 2"/></svg>
              <span>ファイルを選択</span>
              <input type="file" id="garment-image" name="garment_image" accept="image/*" class="hidden" required>
            </label>
            <span id="garment-name" class="ml-2 text-sm text-gray-500"></span>
          </div>
        </div>

        <div class="text-center space-y-3">
          <button type="submit" id="submit-btn"
                  class="bg-gradient-to-r from-indigo-500 to-blue-600 text-white font-bold py-3 px-8 rounded-full hover:shadow-xl transform hover:-translate-y-0.5 transition-all">
            着せ替えを実行
          </button>
          <div>
            <button type="button" id="clear-btn"
                    class="px-4 py-2 text-sm rounded-full border border-gray-300 text-gray-700 hover:bg-gray-50">
              クリア
            </button>
          </div>
        </div>
      </form>

      <div id="result-section" class="mt-10 hidden">
        <h2 class="text-2xl font-bold text-center mb-4 text-gray-800">生成結果</h2>
        <div id="result-display" class="preview-box rounded-lg bg-green-50"></div>
      </div>

      <div id="error-message" class="mt-6 hidden bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded-lg"></div>
    </main>

    <footer class="text-center mt-8 text-gray-500 text-sm">
      <p>Powered by Google Cloud Vertex AI & Cloud Run</p>
    </footer>
  </div>

  <script>
  const personInput = document.getElementById('person-image');
  const garmentInput = document.getElementById('garment-image');
  const personPreview = document.getElementById('person-preview');
  const garmentPreview = document.getElementById('garment-preview');
  const personName = document.getElementById('person-name');
  const garmentName = document.getElementById('garment-name');
  const form = document.getElementById('tryon-form');
  const resultSection = document.getElementById('result-section');
  const resultDisplay = document.getElementById('result-display');
  const errorMessage = document.getElementById('error-message');
  const submitBtn = document.getElementById('submit-btn');
  const clearBtn = document.getElementById('clear-btn');

  function setupPreview(input, previewElement, nameLabel) {
    input.addEventListener('change', (event) => {
      const file = event.target.files[0];
      nameLabel.textContent = file ? file.name : '';
      if (file) {
        const reader = new FileReader();
        reader.onload = (e) => {
          previewElement.innerHTML = `<img src="${e.target.result}" alt="Preview">`;
        };
        reader.readAsDataURL(file);
      } else {
        previewElement.innerHTML = '<span class="text-gray-500">プレビュー</span>';
      }
    });
  }
  setupPreview(personInput, personPreview, personName);
  setupPreview(garmentInput, garmentPreview, garmentName);

  function clearAll() {
    personInput.value = '';
    garmentInput.value = '';
    personName.textContent = '';
    garmentName.textContent = '';
    personPreview.innerHTML = '<span class="text-gray-500">プレビュー</span>';
    garmentPreview.innerHTML = '<span class="text-gray-500">プレビュー</span>';
    resultDisplay.innerHTML = '';
    resultSection.classList.add('hidden');
    errorMessage.classList.add('hidden');
    errorMessage.textContent = '';
  }
  clearBtn.addEventListener('click', clearAll);

  form.addEventListener('submit', async (event) => {
    event.preventDefault();

    const p = personInput.files[0];
    const g = garmentInput.files[0];
    if (!p || !g) return;

    // ★ クライアント側サイズチェック(25MB)
    const MAX = 25 * 1024 * 1024;
    if ((p && p.size > MAX) || (g && g.size > MAX)) {
      errorMessage.textContent = '画像が大きすぎます(25MBまで対応)';
      errorMessage.classList.remove('hidden');
      resultDisplay.innerHTML = '';
      resultSection.classList.add('hidden');
      return; // サーバーへ送らない
    }

    submitBtn.disabled = true;
    submitBtn.textContent = '生成中...';
    resultSection.classList.remove('hidden');
    resultDisplay.innerHTML = '<div class="loader"></div>';
    errorMessage.classList.add('hidden');
    errorMessage.textContent = '';

    const formData = new FormData();
    formData.append('person_image', p);
    formData.append('garment_image', g);

    try {
      const resp = await fetch('/tryon', { method: 'POST', body: formData });
      if (!resp.ok) {
        let msg = `HTTP ${resp.status}`;
        try {
          const j = await resp.json();
          if (j && j.error) msg = j.error;
        } catch {}
        throw new Error(msg);
      }
      const blob = await resp.blob();
      resultDisplay.innerHTML = `<img src="${URL.createObjectURL(blob)}" alt="Result">`;
    } catch (err) {
      console.error(err);
      resultDisplay.innerHTML = '<span class="text-red-500">生成に失敗しました</span>';
      errorMessage.textContent = `エラー: ${err.message}`;
      errorMessage.classList.remove('hidden');
    } finally {
      submitBtn.disabled = false;
      submitBtn.textContent = '着せ替えを実行';
    }
  });
</script>
</body>
</html>
HTML

# requirements.txt
cat > requirements.txt <<'REQ'
Flask==3.0.3
gunicorn==22.0.0
Pillow==10.3.0
google-genai==1.29.0
google-cloud-storage==2.18.2
REQ

# Dockerfile
cat > Dockerfile <<'DOCK'
FROM python:3.11-slim
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends libjpeg62-turbo && rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
ENV PORT=8080 PYTHONUNBUFFERED=1
CMD exec gunicorn -b 0.0.0.0:${PORT} -w 2 --threads 8 --timeout 120 app:app
DOCK

# 作成されたファイルを確認
ls -1

以下のように 4 つのファイル名(app.pyindex.htmlrequirements.txtDockerfile)が表示されていれば成功です!

次に進む前のチェックリスト

  • virtual-tryon フォルダ内に 4 ファイルがある
  • コピペ時に全角クォートへ置き換わっていない(「’」ではなく '

3. コンテナをビルドして Cloud Run にデプロイする

Cloud Shell のターミナルで そのまま丸ごと貼り付けて Enter してください。
(※ 完了までに数分かかりますが、待つだけで OK です)

このコマンドを実行すると、ソースコードのビルド(コンテナイメージと呼ばれるアプリケーションの型を作る)から Web アプリの公開まで自動的に行われます。

# ===== 3-1. 変数を定義 =====
PROJECT_ID="$(gcloud config get-value project)"
REGION="us-central1"           # 変更したい人はここだけ
SERVICE="virtual-tryon"
REPO="app"                     # Artifact Registry のリポジトリ名
TAG="$(date +%Y%m%d-%H%M%S)"
IMG="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/${SERVICE}:${TAG}"
VTO_MODEL="virtual-try-on-preview-08-04"   # ここでモデル名を管理


echo "PROJECT_ID=${PROJECT_ID}"
echo "REGION=${REGION}"
echo "IMAGE=${IMG}"

# ===== 3-2. 必要な API を有効化(実行済みでもそのままでOK)=====
gcloud services enable \
  run.googleapis.com \
  aiplatform.googleapis.com \
  artifactregistry.googleapis.com \
  cloudbuild.googleapis.com

# ===== 3-3. Artifact Registry のリポジトリを作成(初回だけ)=====
gcloud artifacts repositories describe "${REPO}" --location="${REGION}" >/dev/null 2>&1 \
  || gcloud artifacts repositories create "${REPO}" \
       --repository-format=docker \
       --location="${REGION}" \
       --description="App images"

# ===== 3-4. コンテナをビルドしてプッシュ(Cloud Build)=====
cd ~/virtual-tryon
gcloud builds submit --tag "${IMG}"

# ===== 3-5. Cloud Run にデプロイ =====
gcloud run deploy "${SERVICE}" \
  --image "${IMG}" \
  --region "${REGION}" \
  --allow-unauthenticated \
  --set-env-vars PROJECT_ID="${PROJECT_ID}",LOCATION="${REGION}",VTO_MODEL="${VTO_MODEL}"

# ===== 3-6. 公開 URL を確認 =====
gcloud run services describe "${SERVICE}" \
  --region "${REGION}" \
  --format='value(status.url)'

デプロイが成功すると、公開 URL が表示されます。

表示された URL を開くと、UI が表示されます。
あとは「人物画像」と「衣服画像」の 2 枚を選んで 着せ替えを実行を押すだけです。

うまくいかない時は

  • ビルドが長い時は待つだけで OK です。初回はキャッシュがなく数分かかります。

4. 動作確認とチェックポイント

4-1. ブラウザで動作確認

  1. 3 章の最後に表示された URL を開きます。
    例)https://virtual-tryon-xxxxxxxxxx-uc.a.run.app
  2. 「人物画像」と「衣服画像」をそれぞれ選び、着せ替えを実行 をクリックします。
  3. 数十秒で結果画像が表示されれば成功です。

5. よくあるつまずき

5-1. 画像が生成されない(UI は表示されるが 500 エラーになる)

  1. 画像を入れ替えて再実行
    露出・著名人・大きなロゴ・強い加工感は避ける/正面の人物+正面の服が安定。

  2. サイズに注意(25MB まで)
    大きい画像は縮小して再実行。

  3. ブラウザを変える/シークレットモードで試す
    キャッシュの影響を切り分け。

ここまでで解決しなかったら、下の「(上級者向け)ログで原因を特定」を開く(スキップ可)。

5-2. (上級者向け)ログで原因を特定

1) ログを見る(どちらか好きな方法)

CLI 最短

SERVICE="virtual-tryon"
REGION="us-central1"

# エラーだけ追う
gcloud run services logs tail "$SERVICE" --region "$REGION" --level error

# すべて追う(詳細調査)
gcloud run services logs tail "$SERVICE" --region "$REGION"

コンソール
Cloud Console → Cloud Run → 対象サービス → ログタブ → 「エラー」でフィルタ。

コンソール画面の検索バーで「Cloud Run を検索」



対象のサービス名をクリック



「ログ」のタブを選択して、「フィルタ」に検索語句を入力

2) よくある原因と最短対処

  • Not found: Model ...(モデルが見つからない)
    VTO_MODEL が誤り or リージョン不一致。
    対処(再ビルド不要)

    NEW_MODEL="virtual-try-on-preview-08-15"  # 例
    gcloud run services update "$SERVICE" --region "$REGION" \
      --update-env-vars VTO_MODEL="$NEW_MODEL"
    
  • PERMISSION_DENIED / 403(権限不足)
    → サービスアカウントに Vertex AI 権限が不足。
    対処

    SA=$(gcloud run services describe "$SERVICE" --region "$REGION" \
         --format='value(spec.template.spec.serviceAccount)')
    PROJ=$(gcloud config get-value project)
    gcloud projects add-iam-policy-binding "$PROJ" \
      --member="serviceAccount:$SA" --role="roles/aiplatform.user"
    

    (もし GCS 画像の読み取りで 403 が出ていれば↓も付与)

    gcloud projects add-iam-policy-binding "$PROJ" \
      --member="serviceAccount:$SA" --role="roles/storage.objectViewer"
    
  • DeadlineExceeded / 504 / Worker timeout(タイムアウト)
    → モデル処理が長い。
    対処(再ビルド不要)

    gcloud run services update "$SERVICE" --region "$REGION" --timeout 300
    

    ※ さらに長くするなら Dockerfile の --timeout も引き上げ(その場合は再ビルドが必要)。

  • RequestEntityTooLarge / 413(サイズ超過)
    → 画像が大きい。縮小して再試行。

  • 環境変数 PROJECT_ID が未設定です
    → デプロイ時の --set-env-vars を再確認。現在値の確認:

    gcloud run services describe "$SERVICE" --region "$REGION" \
      --format='value(spec.template.spec.containers[0].env)'
    

6. 片付け

ここでは A(プロジェクトは残す)B(プロジェクトごと削除) のどちらかを選びます。
迷ったら B を選べば課金は完全停止します。


A. プロジェクトは残す

6-1 Cloud Run サービスを削除

Cloud Shell で以下のコマンドを実行

SERVICE="virtual-tryon"
REGION="us-central1"
gcloud run services delete "$SERVICE" --region "$REGION" --quiet

6-2 Artifact Registry のイメージを確認(必要なら削除)

Artifact Registry は、コンテナイメージ(作成したアプリの型)を保存するプロジェクト内のプライベートな倉庫です。今回のハンズオンでは、gcloud builds submit(Cloud Build)というコマンドで作ったコンテナイメージを Artifact Registry(例:リポジトリ名 app) にプッシュし、Cloud Run がそこから取得して起動します。(これらは、コピペで実行していただいたコマンドの中に含まれています)

役割|Cloud Build → (保存)Artifact Registry → Cloud Run(起動)
スコープ|リージョンごとにリポジトリを作成(例:us-central1)
構造|リポジトリ/イメージ名:タグ(例:app/virtual-tryon:20250815-120301)
権限|プロジェクトの IAM がそのまま効く(通常は追加設定不要)
費用|保存した分の保管料が少額発生。不要になったら削除で止まる
片付け|記事の「6. 片付け」にある手順で、イメージやリポジトリを削除すればOK
補足|Artifact Registry は旧 Container Registry の後継サービスです。

イメージの削除はコンソール操作が簡単です。
コンソールの検索バー → Artifact Registryを検索 → 対象リポジトリ → イメージを選択 → 削除

Artifact Registry を検索

リポジトリごと削除する場合は、「app」にチェックを入れて削除

指定のイメージだけを削除したい場合は、「app」を開いて、イメージを選択して削除


B. プロジェクトごと削除(完全に課金を止める)

  1. Google Cloud コンソールの左上のナビゲーションメニュー → IAM と管理リソース管理
  2. 対象プロジェクト(例:virtual-tryon-handson)を選択
  3. 右上の シャットダウン をクリック
  4. 画面の指示に従い、プロジェクト ID を入力して このままシャットダウン を実行

コンソール画面の左上の横三本線「ナビゲーションメニュー」をクリック

「IAM と管理」から「リソース管理」を選択

対象のプロジェクトを選択して「削除」

画面の指示に従い、プロジェクト ID を入力して このままシャットダウン を実行

「プロジェクトは削除保留中です」という画面が出たら完了です。

補足
プロジェクトのシャットダウンは 関連リソースをまとめて削除します。30 日以内なら復元できます。


これで片付けは完了です

  • A を選んだ場合は、サービスがなくなり、不要なイメージも整理できます。
  • B を選んだ場合は、すべての課金が止まります(復元しない限り再開しません)。

7. さいごに

ここまで読んでいただき、ありがとうございます。今回は Cloud Run と Vertex AI の Virtual Try-On を使って、ブラウザから画像を送るだけで試着画像を作るところまで一緒に進めました。非エンジニアの方でも、Cloud Shell にコピペで体験できる形にまとめています。少しでも「自分でもやれた」と感じてもらえたなら、とてもうれしいです。

なお、次の一歩として、例えばこんな拡張が考えられます。

  • 画像を Cloud Storage に保存してギャラリー表示
  • アクセス制御を付けて自分だけのミニサービス化
  • 料金の見える化(予算アラートやログ連携)
  • UI の微調整やモバイル最適化

最後に補足です。Virtual Try-On API はプレビュー段階のため、仕様が変わる可能性があります。また、生成に失敗する場合は、記事の「画像選びのコツ」と「よくあるつまずき」を参考に、画像を替えて再試行してみてください。問い合わせや感想はコメントにぜひどうぞ。記事の改善に役立てます。

それでは、みなさんの手で動かした体験が次の学びにつながることを願っています。

最高!動作OKの上で“将来のモデル名変更”まで見据えるのは、とても親切です。読者が迷わないように、記事にそのまま貼れる形で「(スキップ可)」の付録セクションを用意しました。再ビルド不要・数十秒で反映できるやり方を中心にしています。


付録|モデル名の更新方法

このハンズオンでは、ここは実行不要です。将来 Virtual Try-On のモデル名が更新されたときだけ使ってください。
この記事のアプリは、環境変数 VTO_MODEL があればそれを優先し、無ければ app.py の既定値(例:virtual-try-on-preview-08-04)を使います。

A. いちばん簡単|環境変数だけ差し替える(推奨・再ビルド不要)

Cloud Run の サービス設定だけを更新します。コンテナの再ビルドや再プッシュは不要です。

# 1) 変数をセット(あなたの環境に合わせて必要なら変更)
SERVICE="virtual-tryon"
REGION="us-central1"
NEW_MODEL="virtual-try-on-preview-08-15"   # ← 例:新しいモデル名に置き換える

# 2) 環境変数 VTO_MODEL を更新(新しいリビジョンが作られます)
gcloud run services update "$SERVICE" \
  --region "$REGION" \
  --update-env-vars VTO_MODEL="$NEW_MODEL"

# 3) 反映されたか確認(現在の VTO_MODEL を表示)
gcloud run services describe "$SERVICE" \
  --region "$REGION" \
  --format='value(spec.template.spec.containers[0].env[?(@.name=="VTO_MODEL")].value)'

補足:元に戻したい(既定値にフォールバックしたい)場合は次で VTO_MODEL を削除します。

gcloud run services update "$SERVICE" \
  --region "$REGION" \
  --remove-env-vars VTO_MODEL

B. 既定値そのものを更新したい(記事のサンプルコードを書き換える)

この記事のサンプルコードの既定値を書き換える方法です。こちらは再ビルド & 再デプロイが必要です。

  1. app.py の既定値を更新

    # app.py の一行(例)
    VTO_MODEL = os.environ.get("VTO_MODEL", "virtual-try-on-preview-08-15")
    
  2. 2章の作成コマンドを再実行

  3. 3章の手順でビルド → プッシュ → デプロイ
    --set-env-varsVTO_MODEL を含めなくても、新しい既定値が使われます)

C. コンソール(GUI)で更新したい場合

  1. Cloud Console → Cloud Run → 対象サービスを開く
  2. 編集コンテナ環境変数VTO_MODEL を追加/更新
  3. デプロイ(新しいリビジョンが作成されます)

よくある質問

Q. いつ更新が必要?
新しいプレビューが告知された・以前のモデル名が非推奨になった等のタイミングです。まずは A の方法VTO_MODEL を差し替えて試すのが簡単です。

Q. 互換性に注意点は?
多くの場合はモデル名の置き換えだけで OK ですが、まれに仕様が変わることがあります。その場合は公式ドキュメントを参照してください。

Q. どの方法を選べばいい?
すぐ試したい → A(最短・再ビルド不要)
サンプルコードを更新したい → B
GUI で完結したい → C

Discussion