Google Gemini API + Flutterで部屋の家具を自動カウントするアプリを作成してみた

## はじめに

Flutter と Google Gemini APIを使って、部屋の写真をアップロードするだけで家具の数量を自動でリストアップするアプリのモックを作成しました。

本記事では、Google Gemini APIを特定のフォーマットで結果を得るために活用した実装内容と開発を通じて得られた知見をまとめます。

## 対象読者

- Flutterでアプリ開発をしている方
- Google Gemini APIに興味がある方
- 画像認識技術を活用したアプリ開発に興味がある方
- アプリへの生成AIのAPI組み込みに関心がある方

## アプリの概要

このアプリは、ユーザーが部屋の写真をアップロードすると、Google Gemini APIが画像を解析し、部屋に写っている家具の種類と数量をリストアップします。

- 写真のアップロード：ギャラリーまたはカメラから写真を選択
- 家具の自動認識：Google Gemini APIが画像を解析し、家具の種類と数量をリストアップ
- 結果の表示：認識された家具の種類と数量をリストで表示

## 実装

### 使用技術

- Flutter
- Google Gemini API

### 結果取得時のプロンプトの工夫

アプリの作りとしてはシンプルで、Google Gemini APIへリクエストを投げて、そのレスポンスを画面に表示するアプリとなっています。

今回の実装で試した肝になる部分は、Google Gemini APIに対して、特定のJSONフォーマットで結果を返すようにプロンプトで指示することで、アプリ側でのデータ処理を簡略化した点です。

具体的には、家具の状態を管理するために用意した `FurnitureData` クラスで定義した家具の種類と数量をJSON形式で返すようにプロンプトを設計しました。

下記の例では、アップロードされた画像から全てを認識させるプロンプトと、Freezedで指定した家具に特化させたプロンプトをそれぞれ呼び出せるようにして検証しました。

`FurnitureData` クラス（一部抜粋）
```dart
// Package imports:
import 'package:freezed_annotation/freezed_annotation.dart';

part 'furniture_data.freezed.dart';
part 'furniture_data.g.dart';

/// 家具の数量状態の管理
@freezed
@JsonSerializable(explicitToJson: true, createFactory: false)
class FurnitureData with _$FurnitureData {
  // カウントしたい家具を記載
  const factory FurnitureData({
    @JsonKey(name: 'single_seater_sofa') required int singleSeaterSofa,
    @JsonKey(name: 'two_seater_sofa') required int twoSeaterSofa,
    @JsonKey(name: 'bed_single') required int bedSingle,
    @JsonKey(name: 'desk') required int desk,
    @JsonKey(name: 'chair') required int chair,
    ...
  }) = _FurnitureData;

  /// JSONデータからFurnitureDataを作成
  factory FurnitureData.fromJson(Map<String, dynamic> json) =>
    _$FurnitureDataFromJson(json);

  /// プロンプト（認識した全ての物体を判別するパターン）
  static const String extractionPromptAll = '''
    人物や建物など、画像全体から認識できるもの **全て** を教えてください。
    具体的には、画像に写っている人物、建物、乗り物、植物など、可能な限り多くの物体を列挙してください。
    特に、 **家具との位置関係** や **相互作用** にも注意して、詳細な情報を提供してください。
    
    例えば、人物が男性なのか女性なのか、年齢層、服装、何をしているのか（例：ソファに座っている、テレビを見ているなど）を具体的に記述してください。
    車なら車種や色、メーカー、年式が分かるなら車種まで詳細に、
    料理なら料理名、材料、調理方法が分かるなら料理名まで詳細に、
    植物の名前がわかるなら植物名、種類、状態（例：開花している、枯れているなど）まで記述してください。
    
    画像全体の **雰囲気** や **状況** を考慮して、オブジェクト同士の関係性やストーリーを推測してください。
    例えば、家族写真であれば、家族構成や関係性を推測したり、
    風景写真であれば、場所、時間帯、天候などを推測したりするなど、
    画像から読み取れる情報を最大限に活用してください。
    
    複数や多数の場合も、必ず数値でvalueを返してください。
    出力はJSONでお願いします。keyを認識対象、valueを数量としてください。
    
    出力フォーマット：
    {
      "ソファに座って本を読んでいる20代女性": 1,
      "テレビを見ている30代男性": 1,
      "茶色の革製ソファ": 1,
      "50インチのSony製テレビ": 1,
      "開いている窓から見える晴れた空": 1,
      "ソファの横で寝ているゴールデンレトリバー": 1
    }
  ''';

  /// プロンプト（特定の家具だけ認識するパターン）
  static const String extractionPrompt = '''
    前回の依頼プロンプトここから
    $extractionPromptAll
    前回の依頼プロンプトここまで
    
    前回は画像全体から認識できるもの **全て** と依頼しましたが、
    今回は出力フォーマットに示す家具の分類に絞って数量を教えてください。
    前回の認識結果を参考に、以下の家具の数をカウントしてください。
    
    複数や多数の場合も、必ず数値でvalueを返してください。
    受け取った結果をそのまま扱いたいので、 ** JSON形式で{}の前後に何も文字を含まないでください。**
    {}の前にjsonなどの文字列も不要です。
    
    出力フォーマット：
    {
      "single_seater_sofa": 0,
      "two_seater_sofa": 0,
      "bed_single": 0,
      "desk": 0,
      "chair": 0,
      ...
    }
  ''';
}

```

これにより、下記のようにAPIからのレスポンスをそのまま `FurnitureData` クラスのインスタンスにマッピングすることができ、アプリ側での複雑なパース処理が不要になりました。

```dart
Future<FurnitureData> fetchFurnitureList(File imageFile) async {
  final response = await requestGemini(imageFile, FurnitureData.extractionPrompt);
  // Gemini APIからのレスポンス（JSON文字列）をFurnitureDataオブジェクトにそのまま変換
  return FurnitureData.fromJson(jsonDecode(response.text));
}
```

### Google Gemini APIの呼び出し

今回はGoogle Gemini APIの呼び出しにはFlutterパッケージの [google_generative_ai](https://pub.dev/packages/google_generative_ai) で検証しました。

:::note warn
上記リンクにも記載されている通り、Google AI SDK for Dart (Flutter)を使用して、アプリから Google AI Gemini API を直接呼び出すことは、プロトタイピングのみに推奨されますのでご注意ください。
:::


```dart
// Dart imports:
import 'dart:developer';
import 'dart:io';
import 'dart:typed_data';

// Package imports:
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:google_generative_ai/google_generative_ai.dart';

/// Gemini にリクエストを送信する
Future<GeminiResponse> requestGemini(File image, String prompt) async {
  final generationConfig = GenerationConfig(
    maxOutputTokens: 1000, temperature: 0.5, topP: 0.2, topK: 15);

  final model = GenerativeModel(
    model: 'gemini-1.5-pro-latest',
    apiKey: dotenv.get('API_KEY'),
    generationConfig: generationConfig);

  List<Part> parts = [TextPart(prompt)];
  Uint8List imageBytes = await image.readAsBytes();
  parts.add(DataPart('image/jpeg', imageBytes));

  final response = await model.generateContent([Content('user', parts)]);

  log('Gemini response: ${response.text}');

  return GeminiResponse(response.text.toString());
}

/// Gemini のレスポンス
class GeminiResponse {
  final String text;
  GeminiResponse(this.text);
}
```

プロンプトと画像イメージを `google_generative_ai` にリクエストするだけで結果を得ることができます。

これにより、取得した家具の一覧を簡単にアプリ上にドロップダウン形式などでも出力することができます。

## 参考資料
[Google Gemini API](https://ai.google.dev/gemini-api/docs?hl=ja) ドキュメント
[Flutter](https://docs.flutter.dev/) ドキュメント

## おわりに
Flutter と Google Gemini APIを組み合わせることで、部屋の写真をアップロードするだけで簡単に家具の数量を自動でリストアップするアプリを作成することができました。

家具の数量リストアップは今回の題材の一例であり、Google Gemini APIは今回示したように、指定したフォーマットで結果を得ることができるので、用途に併せて比較的簡単にGoogle Geminiをアプリに導入して独自の扱い方で活用することができます。

今回はFreezedで定義したモデルに合うようなJSONを返すようにプロンプトを工夫してGeminiに指示を出すことで、アプリ側での実装を簡易化できることを検証しました。
プロンプトの工夫次第でアプリとしてもっと扱いやすくなるかなど検証してみようと思います。