generate-images-with-firebase-ai について
generate-images-with-firebase-ai は、Dart と Firebase AI Logic で画像生成機能を構築するためのスキルです。リクエスト設定、テキストと画像の混在レスポンス、サイズ調整、プロンプト、テストを解説します。
主な機能
- 01画像生成リクエストの設定
- 02テキストと画像パーツが混在する応答の解析
- 03空の応答やブロックされた応答の診断
- 04入力写真の準備と出力サイズの調整
- 05生成画像とテキストデータの組み合わせ
活用シーン
- 01Dart アプリへの Gemini 画像生成の追加
- 02応答から画像バイト列とテキストを抽出
- 03課金、App Check、モダリティ設定の問題調査
- 04応答解釈のテストとプロンプトの改善
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ evanca/ flutter-ai-rules --skill generate-images-with-firebase-aiインストール後、Claude Code で/generate-images-with-firebase-aiを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/models.mdprompting.mdsetup.mdtesting.md詳細
Firebase AI Logic で画像を生成する
Gemini の画像モデルは、1 回の呼び出しでテキストと画像のパートを交互に返します。レスポンスは、読み取る文字列ではなく、順にたどるシーケンスです。
空のレスポンスが返ってくる場合、通常はコードのバグではなく設定の問題です。その原因となる 3 つの設定をセクション 1 で説明します。
1. 最初の呼び出しを妨げる 3 つの要因
最初の呼び出しでエラー、空のレスポンス、または 403 が返る場合、ほぼ間違いなく Dart ではなく、次のいずれかが原因です。コードをデバッグする前に、これらを除外してください。
課金。 画像生成に無料枠はありません。Spark プランのプロジェクトでは、画像モデルの generate_content_free_tier_requests の limit: 0 が返されるため、リクエストを 1 回も送っていない状態でも、最初のリクエストはクォータ超過で失敗します。Spark ではテキストモデルは動作するため、「ほかの Gemini 呼び出しは動く」ことは何の証明にもなりません。Blaze にアップグレードし、この記述を鵜呑みにせず、現在の制限を確認してください。
gcloud services quota list --service=generativelanguage.googleapis.com --consumer=projects/YOUR_PROJECT_ID
App Check。 プロジェクトで App Check が有効になっていると、Firebase AI Logic によって App Check が適用されます。そうでない場合、出荷済みクライアントから設定を取得した人なら誰でもエンドポイントを利用できます。この設定は意図的に公開されています。管理できないデバイスからアクセス可能なものには、App Check が必要です。デバッグビルドではデバッグプロバイダーで、リリースビルドでは実際のプロバイダーで認証します。Web には、午後いっぱいを費やしかねない特有の落とし穴があります。references/setup.md で説明しています。
responseModalities。 これがないと、モデルは画像を返す権限がないため、描画するはずだった画像の説明テキストが返されます。以下の呼び出し例のように、両方のモダリティを設定してください。
2. 動作する最小限の呼び出し
final model = FirebaseAI.googleAI().generativeModel(
model: 'gemini-3.1-flash-image',
generationConfig: GenerationConfig(
responseModalities: [
ResponseModalities.text,
ResponseModalities.image,
],
imageConfig: ImageConfig(
aspectRatio: ImageAspectRatio.landscape16x9,
imageSize: ImageSize.size2K,
),
),
);
final response = await model.generateContent([
Content.multi([
TextPart(prompt),
InlineDataPart('image/jpeg', selfieBytes), // omit for text-to-image
]),
]);
FirebaseAI.googleAI() は Gemini Developer API です。こちらを推奨します。独自の GCP 環境を必要とせず、無料枠でテキストを利用できます。FirebaseAI.vertexAI() はモデルにかかわらず Blaze が必要で、GCP 側の制御機能を利用できます。そのため、デフォルトで選ぶのではなく、プロジェクトがすでに Vertex 上にある場合に使用してください。
現在の firebase_ai では、googleAI() に appCheck: や auth: を渡さないでください。どちらのパラメーターも非推奨です。インスタンスが FirebaseApp から自動的に解決します。
モデル ID、アスペクト比とサイズの列挙型、Imagen が Gemini より適している状況については、references/models.md を参照してください。
3. レスポンスの読み取り
簡易アクセス用の .text ではシーケンス全体を取得できないため、パートを直接たどってください。パートの型に応じてパターンマッチングすると、SDK に新しいパート型が追加されても switch が正しく機能します。
Uint8List? image;
final buffer = StringBuffer();
for (final part in candidate.content.parts) {
switch (part) {
case InlineDataPart(:final bytes):
image ??= bytes; // first image wins
case TextPart(:final text):
buffer.write(text);
default:
break;
}
}
レスポンスの解釈は、Candidate を受け取る独立した純粋関数にまとめてください。Candidate、Content、TextPart、InlineDataPart はすべて公開コンストラクターで生成できるため、Firebase もテストダブルも使わず、実際の SDK 型でその関数をテストできます。このスタックで、ユニットテストが実際に到達できる唯一の継ぎ目です。
画像が返ってこない場合
空の結果は空白のエラーとして表面化するため、クライアントのバグのように見えます。その結果、システムの誤った側をデバッグすることになります。理由はレスポンスに含まれているため、それを報告してください。次の順に確認します。
| 確認項目 | 場所 | 意味 |
|---|---|---|
response.promptFeedback?.blockReason | 候補より前 | 入力が拒否されました。blockReasonMessage も確認してください。 |
response.candidates が空 | 該当なし | 何も生成されませんでした。 |
candidate.finishReason | 候補上 | モデルが停止しました。安全性、再現、またはトークン上限が原因です。finishMessage に詳細が含まれます。 |
| 画像はないがテキストはある | パートをたどった後 | 描画ではなく、文章で回答しました。通常はプロンプトの問題です。 |
| すべて空で、理由もない | 該当なし | そのまま明確に伝え、ユーザーが再試行できるようにしてください。これは断続的に発生します。 |
両方を安定して返すプロンプトでも、テキストがまったくない画像のみのレスポンスが断続的に発生します。画像と一緒にテキストを求めた場合、その欠如は正常なものとして扱い、例外をスローせずに機能を縮退させてください。
4. ユーザーの写真を送信する
送信する前に縮小してください。InlineDataPart.toJson()はメインのアイソレートでバイト列を同期的にBase64エンコードするため、サイズそのままのスマートフォン写真を使うと、リクエストの構築中にUIが数秒間フリーズします。Geminiは大きな画像をいずれにせよダウンサンプリングするため、後で破棄されるディテールに料金を払うことになります。選択後ではなく、選択時にサイズを調整してください:
final file = await picker.pickImage(
source: ImageSource.gallery,
maxWidth: 1280,
maxHeight: 1280,
imageQuality: 85,
);
上限サイズは、いつもの設定ではなく被写体に合わせてください。長辺が1280pxなら、顔や人物1人を十分な大きさで表示できます。一方、細かな質感や元画像内の読み取り可能なテキスト、モデルが読み取る必要のある広い範囲のシーンでは、より大きなサイズが必要です。mimeTypeは、ピッカーが実際に返した形式に合わせてください。
5. 結果のサイズを決める
比率がサポート対象の列挙値のいずれかであれば、実際の制約として機能するImageConfigを使用してください。プロンプト内で比率を指定しても提案にすぎず、モデルが無視することがよくあります。列挙値にない比率、たとえば2:1が必要な場合は、生成された画像を計測し、その計測値をもとにレイアウトしてください:
final descriptor = await ui.ImageDescriptor.encoded(
await ui.ImmutableBuffer.fromUint8List(bytes),
);
final size = ui.Size(descriptor.width.toDouble(), descriptor.height.toDouble());
descriptor.dispose();
失敗した場合に例外を投げず、nullを返すようにラップしてください。計測できない画像でも、表示する価値はあります。
6. 1回の呼び出しでテキストと画像を取得する
モデルが描いた画像に関する、キャプションやモデルが付けた名前などの機械可読データも必要になることがよくあります。データがピクセルになるとアプリから読み取れなくなるため、同じ呼び出しでテキストとして取得し、プロンプトで両者を一致させるよう指示してください。「画像に描かれたタイトルは、JSONと一字一句同じでなければなりません。」
そのテキストは慎重に解析してください。```jsonのフェンス付きブロックを求めても、モデルは同じセッション中に、フェンス付きJSON、フェンスなしのJSON、説明文で囲まれたJSON、そして何も返さない場合を見せることがあります。まずフェンスを試し、次に最初の対応する{...}を探し、それでも見つからなければ例外ではなくnullを返してください。
7. プロンプトの作成
ここで起きる失敗はコードの不具合ではなく、ユニットテストでも検出できません。繰り返し発生する失敗には、それぞれ固有の対策があります。最初のプロンプトを書く前に知っておく価値があります。プレースホルダーの単語がそのままアートワークに描かれる、衣装を変更しても元写真の服が残る、同じテキストが2か所で異なる表記になる、パネルごとに背景が揃わない、といった問題です。references/prompting.mdを読んでください。
8. テスト対象を決める
境界にあるのがテストの分岐点です。ユニットテストで保護できるのはレスポンスの解釈までで、その先は対象外です。そのため、テストがすべて成功しても、アプリが良い画像を生成するかどうかは分かりません。references/testing.mdでは、各レイヤーとそれぞれの限界について説明しています。
外部ドキュメント
ここに記載した内容はすべて概要であり、いずれ古くなります。詳細が重要な場合は、必ず一次情報で確認してください。
- Firebase AI Logic のドキュメント。使ってみるとGemini で画像を生成するも含まれます
- Dart SDKの
firebase_ai。APIに関する疑問を解決するには、ソースを確認するのが最も手早い方法です:~/.pub-cache/hosted/pub.dev/firebase_ai-*/lib/src/ image_picker。セクション4のpickImage呼び出しを提供します。差し替え可能なサードパーティ製の選択肢です。- Patrol。LeanCodeによるサードパーティ製E2Eフレームワークで、
references/testing.mdで取り上げています
リファレンスファイル
references/setup.md:Firebaseコンソールでの設定手順、プロバイダーの選択、課金、ウェブ版のApp Checkデバッグトークンの落とし穴references/models.md:モデルID、アスペクト比とサイズの列挙値、GeminiとImagenreferences/prompting.md:画像プロンプトで実際に起きる失敗と、それを防ぐ表現references/testing.md:ユニット、ゴールデン、E2E、評価の各レイヤーと、それぞれの限界