Flutter × Genkit × Imagen 3 で始める AI 画像生成アプリ開発入門
本記事は Flutter 大学アドベントカレンダー 8 日目の記事です。
はじめに
最近 Firebase Genkit を触って遊ぶことが多い cobo です。
ありがたいことに、先日は GDG DevFest Tokyo 2024 にて Genkit に関する LT をさせていただきました。
本記事では、上記 LT でも紹介した、Genkit から Imagen 3 を参照するサーバーサイドプログラムを作成することに加え、それを Flutter 上から呼び出すというアプリを紹介します。
Genkit とは
AI 機能を開発・デプロイするためのオープンソースフレームワークです。
プラグインやテンプレート、シンプルな抽象化層を提供することで、AI モデルとカスタムロジックを組み合わせた機能開発を容易にします。
今回は、Genkit で作成した関数をクラウド上 (Cloud Run) にデプロイします。
Imagen 3 とは
Google Cloud の Vertex AI が提供する画像生成モデルです。
テキストプロンプトから高品質な画像を生成することができ、写実的な写真からイラストまで幅広い表現が可能です。
この記事で作成するアプリ
テキストプロンプトから画像を生成できる Flutter アプリを作成します。
入力欄にプロンプトを入力して送信すると、Imagen 3 が AI で画像を生成してくれます。
バックエンドには Genkit を使用して、Cloud Run 上で動作させます。
(最近、冬の澄んだ空が綺麗なので、clear winter sky
と入力しています。)
アーキテクチャ
- クライアントサイド
- Flutter アプリケーション: Cloud Run 上の Genkit 関数をコールする
- サーバーサイド
- Cloud Run: Vertex AI の Imagen 3 に問い合わせするための Genkit 関数が動作する
- Vertex AI: Imagen 3 のモデルを使用して AI 画像生成を行う
実装手順
概要が理解できたところで、実装に移っていきましょう。
大まかに次の手順に分かれます。
- Google Cloud プロジェクトの用意
- Vertex AI に関する API の有効化
- Flutter プロジェクトのセットアップ
- Genkit プロジェクトのセットアップ
- 画像生成関数の実装
- Genkit ローカルエミュレータの実行
- Cloud Run へのデプロイ
- Flutter アプリの実装
0. Google Cloud プロジェクトの用意
Cloud Run や Vertex AI を動作させるための Google Cloud プロジェクトを作成します。
1. Vertex AI に関する API の有効化
Vertex AI を動作させるために必要な API を有効化します。
Google Cloud の Vertex AI のコンソールから可能です。
2. Flutter プロジェクトのセットアップ
上記 API の有効化が完了するまでに 3 分程度かかるので、並行して Flutter プロジェクトの初期化も進めておきます。
デフォルトのカウンターアプリが起動する状態です。
3. Genkit プロジェクトのセットアップ
今回は、Flutter プロジェクトの中に Genkit ディレクトリを切る形でセットアップします。
flutter_imagen3/ # Flutter プロジェクトのルート
├── lib/ # Flutter アプリのソースコード
├── pubspec.yaml # Flutter の依存関係
├── genkit/ # Genkit プロジェクト
│ ├── package.json
│ └── src/
│ └── index.ts # Genkit 関数の実装
└── ...
mkdir genkit
cd genkit
npm init -y
npm install typescript @types/node --save-dev
npx tsc --init
genkit 配下の設定ファイルなどは下記のサンプルコードを参考にしてください。
4. 画像生成関数の実装
genkit/src/index.ts
に Vertex AI 内の Imagen 3 をコールするための Genkit 関数を実装します。
// 必要なパッケージのインポート
import {genkit, z} from "genkit";
import {vertexAI} from "@genkit-ai/vertexai";
import {logger} from "genkit/logging";
// Genkit の初期化と Vertex AI プラグインの設定
const ai = genkit({ plugins: [vertexAI()] })
logger.setLogLevel("debug")
// 画像生成用の Genkit Flow を定義
export const generateImageFlow = ai.defineFlow(
{
name: "generateImage",
inputSchema: z.object({
imageDescription: z.string(), // クライアントから受け取る画像の説明文
}),
},
async (input) => {
// Imagen 3 へ送信するプロンプトの生成
const prompt = `You are the best Japanese manga artist in the world.
Please generate an image of ${input.imageDescription}.`;
// Vertex AI 内の Imagen 3 による画像生成の実行
const response = await ai.generate({
model: `vertexai/imagen3`, // 使用モデル
prompt: prompt, // 生成プロンプト
config: {
temperature: 0.7, // 生成時の創造性の度合い(0.0 - 1.0)
},
output: {
format: `media`, // 出力形式を画像データに指定
},
});
console.log('Response:', response);
return response.media;
}
)
// サーバーの起動設定
ai.startFlowServer({
flows: [generateImageFlow], // 使用する Genkit 関数の登録
cors: {
origin: true
}
})
5. Genkit ローカルエミュレータの実行
実装した関数をローカル環境で動作確認します。
genkit
ディレクトリで以下のコマンドを実行します。
genkit start -- npx tsx --watch src/index.ts
正常に起動すると localhost でサーバーが立ち上がります。
先ほど、実装した Genkit 関数 generateImage
が一覧に表示されるので選択します。
クライアントからの振る舞いを記述します。
Genkit 関数では、imageDescription
というインタフェースを公開しているので、clear winter sky
と入力してみます。
その後、Run ボタンをクリックします。
数秒後、Imagen 3 によって画像が生成されたことが確認できます。
6. Cloud Run へのデプロイ
ローカルでの動作確認が完了したら、Cloud Run にデプロイします。
genkit
ディレクトリで以下のコマンドを実行します。
gcloud init
gcloud run deploy
デプロイ先のリージョンは asia-northeast1
としました。
5 分程度待つと Genkit 関数が乗った Cloud Run がデプロイされます。
また、この Cloud Run はクライアントユーザーからの呼び出しになるので、allUsers
に対して Cloud Run Invoker (Cloud Run 起動元)
のロールを付与します。
7. Flutter アプリの実装
最後に、Flutter アプリから Cloud Run にデプロイした API を呼び出す実装を行います。
サンプルコードリポジトリも用意しているので重要な部分だけ紹介します。
import 'package:dio/dio.dart';
/// Genkit 経由で Imagen 3 を使用して画像を生成するためのクライアント
class GenkitClient {
GenkitClient({
required this.dio,
});
final Dio dio;
/// 与えられた説明文から画像を生成し、Base64 エンコードされた画像データを返却します
///
/// [imageDescription] 生成したい画像の説明文
/// 戻り値: 'data:image/png;base64,...' 形式の Base64 エンコードされた画像データ
Future<String> generateImage({required String imageDescription}) async {
try {
final response = await dio.post(
'https://<YOUR_GENKIT_ENDPOINT>/generateImage',
data: {
'data': {
'imageDescription': imageDescription,
},
},
);
if (response.statusCode == 200) {
return response.data['result']['url'] as String;
}
throw Exception('Failed to generate image: ${response.statusCode}');
} on DioException catch (e) {
throw Exception('Failed to generate image: ${e.message}');
}
}
}
import 'package:flutter/material.dart';
import 'package:flutter_imagen3/ui/components/generated_image_view.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../usecase/generate_image_usecase.dart';
final generatedImageUrlProvider = StateProvider<String?>((ref) => null);
class ImageGeneratorPage extends ConsumerWidget {
ImageGeneratorPage({super.key});
final _controller = TextEditingController();
Widget build(BuildContext context, WidgetRef ref) {
final imageUrl = ref.watch(generatedImageUrlProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Imagen 3 Demo'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
TextField(
controller: _controller,
decoration: InputDecoration(
labelText: 'Image Description',
hintText: 'Enter what you want to generate',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
filled: true,
),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () async {
{
// テキスト入力を使って画像生成ユースケース(GenkitClient の呼び出しをカプセル化)
// 生成された画像URLを状態に反映を呼び出し
final generatedImageUrl = await ref
.read(generateImageUsecaseProvider)
.invoke(imageDescription: _controller.text);
ref.read(generatedImageUrlProvider.notifier).state =
generatedImageUrl;
}
},
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 32,
vertical: 16,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: const Text('Generate Image'),
),
const SizedBox(height: 16),
if (imageUrl != null)
Expanded(
child: GeneratedImageView(imageUrl: imageUrl),
),
],
),
),
);
}
}
Discussion