CCCMKホールディングス TECH LABの Tech Blog

TECH LABのエンジニアが技術情報を発信しています

ブログタイトル

Databricks Appsで動く、画像編集チャットアプリを開発してみました。

こんにちは、CCCMKホールディングスAIエンジニアの三浦です。

この前の休日はとても暖かくて近所の公園ではたくさんの人がテントを張ってピクニックをしていました。外でご飯を食べるのってなんだか特別な感じがしていいなぁと思います。今度晴れたらやってみようかな、と思いました。

さて今回の記事ではDatabricksとGithub Copilotの勉強を兼ねて最近取り組んでいた、画像編集チャットアプリ開発の話をさせてください。

どんなアプリ?

開発したのは資料などに使う画像をちょっと編集したい時に使えそうなアプリです。

アプリに編集したい画像をアップロードするとチャット画面が開き、編集したい内容をテキストで指示することが出来ます。

画像編集チャット画面

すると指示したとおりに編集された画像が生成されます。

指示通りに画像が編集される

一部の文字を変える、といったことも対応しています。

文字を編集

編集した画像はダウンロードして使えるようにしました。

元の画像とアプリで編集された画像

このアプリはDatabricks Appsで動いています。システム構成はざっくりと以下のようになっています。

Databricksを中心にしたシステム構成

使った技術

このアプリで重要なのは画像とプロンプトを受け取り、プロンプト通りに画像を編集する処理です。こちらはHugging Faceで公開されているQwen-Image-Edit-2511というモデルを使いました。

huggingface.co

このモデルをDatabricksのUnity CatalogのModelsに登録し、Model Serving EndpointにデプロイしてHTTP Requestで呼び出せるようにしました。

Qwen-Image-Edit-2511は対応している言語が英語と中国語のようなので、日本語で入力されたプロンプトを一度英語に翻訳する必要がありました。その処理はDatabricksのFoundation Model APIで利用可能なLLM(databricks-claude-sonnet-4-5)を使って実現しました。

アプリ自体はhtmxとfastapiで実装し、Databricks Appsにデプロイして動かしました。

そして開発はM365 Copilotを使って情報収集や検証用のNotebookを作成、アプリはGithub Copilotで実装する、という進め方をしてみました。

アプリ開発の進め方

Qwen-Image-Edit-2511の検証

まずはアプリの中心となる画像編集部分に使用するQwen-Image-Edit-2511の検証をAzure DatabricksのNotebook上で行いました。

ここで検証したのは

  • モデルの呼び出し方の確認
  • モデルを動かせるGPUの確認
  • モデルの画像編集精度の確認

です。特にGPUについては最初もしかしたらT4でも動くかな?と思っていたのですが、検証するとOut-Of-Memoryエラーが出てしまいA100が必要になることが分かりました。

今回はこの結果からQwen-Image-Edit-2511を動かすModel Serving EndpointのComputeを"GPU Large (A100)"にすることに決めたのですが、今後はもう少し小さいサイズのGPUでも動かせるようにQwen-Image-Edit-2511を量子化する工夫もしたいなと思っています。

Model Serving Endpointへのデプロイ

Qwen-Image-Edit-2511をMLflowのpyfuncクラスから呼び出すように実装し、MLflowでUnity CatalogのModelsに登録しました。

この作業で詰まるところがありました。前のステップでA100でQwen-Image-Edit-2511が動くことは確認していたのですが、mlflow.pyfunc.log_modelを実行するとなぜかOut-Of-Memoryが発生してしまいました。

具体的には以下のようなコードです。

with mlflow.start_run() as run:
    mlflow.pyfunc.log_model(
        artifact_path="model",
        python_model=QwenImageEditPyfunc(),
        code_paths=[MODEL_CODE_DIR],
        pip_requirements=pip_reqs,
        input_example=example_df,
    )
    model_uri = f"runs:/{run.info.run_id}/model"
    print("Logged model:", model_uri)

原因はinput_exampleを指定しているところで、これによってmlflow.pyfunc.log_model実行時にinput_exampleに対する推論処理を実行するためだけにQwen-Image-Edit-2511がGPUにロードされてしまいOut-Of-Memoryが起きているようです。

結局、入出力をSignatureで定義して渡すようにしてinput_exampleを指定することを避け、対応しました。

from mlflow.models import ModelSignature
from mlflow.types import Schema, ColSpec

signature = ModelSignature(
    inputs=Schema([
        ColSpec("string", "payload"),
    ]),
    outputs=Schema([
        ColSpec("string", "request_id"),
        ColSpec("string", "status"),
        ColSpec("string", "edited_image_b64"),
        ColSpec("string", "meta"),
        ColSpec("string", "error"),
    ]),
)

pip_reqs = [
    "mlflow",
    "pandas",
    "pillow",
    "safetensors",
    "accelerate",
    "transformers",
    "git+https://github.com/huggingface/diffusers",
]

with mlflow.start_run() as run:
    mlflow.pyfunc.log_model(
        artifact_path="model",
        python_model=QwenImageEditPyfunc(),
        code_paths=[MODEL_CODE_DIR],
        pip_requirements=pip_reqs,
        signature=signature,
    )
    model_uri = f"runs:/{run.info.run_id}/model"
    print("Logged model:", model_uri)

registered_model = mlflow.register_model(
    model_uri=model_uri,
    name=UC_MODEL_FULL_NAME
)

print("Registered:", registered_model.name, "version:", registered_model.version)

上記が正常に完了した後、以下のコードでModel Serving Endpointにデプロイしました。

# Model Servingにデプロイする
import time
import mlflow.deployments

client = mlflow.deployments.get_deploy_client("databricks")

endpoint_name = "qwen-image-edit-endpoint"

ep = client.get_endpoint(endpoint_name)
exists = ep is not None

if exists:
    client.delete_endpoint(endpoint_name)

for i in range(30):
    time.sleep(5)
    try:
        client.get_endpoint(endpoint_name)
        print(f"  waiting delete... ({i+1})")
    except Exception:
        print("Delete completed")
        break

# 例: UC登録済みモデル
served_model_name = UC_MODEL_FULL_NAME 
served_model_version = str(registered_model.version)

config = {
    "served_entities": [
        {
            "entity_name": served_model_name,
            "entity_version": served_model_version,
            "name": "qwen-image-edit-1",
            "workload_type": "GPU_LARGE",   # A100系
            "workload_size": "Small",
            "scale_to_zero_enabled": False, 
        }
    ]
}

client.create_endpoint(name=endpoint_name, config=config)
print("Creating endpoint:", endpoint_name)

Github Copilotでアプリ実装

ここからはGithub Copilotを使ってどんな方針や進め方でアプリ開発を進めたのかをご紹介します。

参考ドキュメントをまとめる

Model Serving Endpointを構築した後は、それを呼び出すアプリケーションの開発に進みました。開発はGithub Copilotを使用したのですが、これまでの経験上Databricks固有の機能についてはGithub Copilotの知識が古かったり間違っていることが多い印象があったので、まず最初に特に引っかかりそうな情報を調べ、マークダウン形式のドキュメントにまとめておきました。

特に以下の情報について調べてまとめておきました。

  • Databricks Appsでのログインユーザー情報の取得方法
  • Databricks Workspace Clientを使ったModel Serving/Foundation Modelの呼び出し方法
  • Model Serving EndpointのAPI仕様

そしてGithub CopilotのAgentに開発依頼を出す際にこれらの参考ドキュメントを参照するよう指示を出すようにしました。

画面モックアップから着手

今回の開発は、先に画面のモックアップ作成に着手するようにしました。この進め方はよかったな、と思っています。以降の開発を進める際に「この画面のこの機能の実装」のように開発の範囲を具体的に明文化出来るようになったからです。AIを使って開発する際に、「明文化する」はとても大事なことだと感じています。

タスクを細かく設定し明文化

私たちのチームではリモートレポジトリやタスク管理にAzure DevOpsを利用しています。先ほどの話と通ずる内容ですが、今回の開発ではかならずAIに出す指示をタスクに「明文化」してから進めるようにしました。

また、タスクには「やること」に加え「求める成果物」そして「完了条件」を必ず含めるようにしました。タスクを進めていると「あ、これもやらないと」といった別の課題が時々発生します。そういった追加の課題を同じタスクの中で取り組んでしまうと一回のコミットやPRが長くなり過ぎてしまいますし、AIが扱うコンテキストが膨大になって作業精度が落ちてしまいます。

「成果物」「完了条件」を明確にしておくことで、新しい課題は別のタスクを作ってそこで定義する、という意識を保つことが出来るようになりました。

こうしてAzure DevOpsのWork Itemsとして設定したタスクの内容を起点にGithub Copilotに開発を依頼、開発が終わったらPRを出してもらって確認する、といったことを繰り返して開発を進めていきました。Azure DevOpsのWork Itemsの取得やPRの発行はチームのメンバーが作ってくれた専用のAgent Skillsを利用しました。

まとめ

今回はDatabricks Appsで動く画像編集チャットアプリの開発についてご紹介しました。DatabricksにはAI系の機能が豊富に揃っているのでこういったアプリを動かすには最適な環境だと思います。また、Github Copilotを使った開発も少しずつですが慣れてきたように感じています。新しい機能がどんどん追加されているので、それらを活用してもっと開発精度や効率を向上していきたいです。