🔥

Flutter × Genkit × Imagen 3 で始める AI 画像生成アプリ開発入門

2024/12/08に公開

本記事は 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 上で動作させます。

image-generator
(最近、冬の澄んだ空が綺麗なので、clear winter sky と入力しています。)

アーキテクチャ

architecture

  • クライアントサイド
    • Flutter アプリケーション: Cloud Run 上の Genkit 関数をコールする
  • サーバーサイド
    • Cloud Run: Vertex AI の Imagen 3 に問い合わせするための Genkit 関数が動作する
    • Vertex AI: Imagen 3 のモデルを使用して AI 画像生成を行う

実装手順

概要が理解できたところで、実装に移っていきましょう。
大まかに次の手順に分かれます。

  1. Google Cloud プロジェクトの用意
  2. Vertex AI に関する API の有効化
  3. Flutter プロジェクトのセットアップ
  4. Genkit プロジェクトのセットアップ
  5. 画像生成関数の実装
  6. Genkit ローカルエミュレータの実行
  7. Cloud Run へのデプロイ
  8. Flutter アプリの実装

0. Google Cloud プロジェクトの用意

Cloud Run や Vertex AI を動作させるための Google Cloud プロジェクトを作成します。

create-project

1. Vertex AI に関する API の有効化

Vertex AI を動作させるために必要な API を有効化します。
Google Cloud の Vertex AI のコンソールから可能です。

vertex-ai-api

2. Flutter プロジェクトのセットアップ

上記 API の有効化が完了するまでに 3 分程度かかるので、並行して Flutter プロジェクトの初期化も進めておきます。
デフォルトのカウンターアプリが起動する状態です。

setup-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 関数を実装します。

genkit/src/index.ts
// 必要なパッケージのインポート
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-start

先ほど、実装した Genkit 関数 generateImage が一覧に表示されるので選択します。

select-flows

クライアントからの振る舞いを記述します。
Genkit 関数では、imageDescription というインタフェースを公開しているので、clear winter sky と入力してみます。
その後、Run ボタンをクリックします。

genkit-run

数秒後、Imagen 3 によって画像が生成されたことが確認できます。
generated-image

6. Cloud Run へのデプロイ

ローカルでの動作確認が完了したら、Cloud Run にデプロイします。
genkit ディレクトリで以下のコマンドを実行します。

gcloud init
gcloud run deploy

デプロイ先のリージョンは asia-northeast1 としました。

5 分程度待つと Genkit 関数が乗った Cloud Run がデプロイされます。

cloud-run-service

また、この Cloud Run はクライアントユーザーからの呼び出しになるので、allUsers に対して Cloud Run Invoker (Cloud Run 起動元) のロールを付与します。

add-role

7. Flutter アプリの実装

最後に、Flutter アプリから Cloud Run にデプロイした API を呼び出す実装を行います。
サンプルコードリポジトリも用意しているので重要な部分だけ紹介します。

Flutter からの Cloud Run 呼び出し
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}');
    }
  }
}
GenkitClient のコールおよび生成画像の描画
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),
              ),
          ],
        ),
      ),
    );
  }
}

サンプルコード

GitHubで編集を提案

Discussion