generate-images-with-firebase-ai

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Generating images with Firebase AI Logic

使用Firebase AI Logic生成图像

Gemini image models return interleaved text and image parts from one call. The response is a sequence to walk, not a string to read.
A request that comes back empty is usually a configuration problem rather than a bug in your code, so section 1 covers the three settings that cause it.
Gemini图像模型单次调用会返回交错的文本和图像片段。响应是需要遍历的序列,而非可直接读取的字符串。
返回空结果的请求通常是配置问题而非代码bug,因此第1节会介绍导致该问题的三个配置项。

1. Three things that block the very first call

1. 阻碍首次调用的三个常见问题

A first call that returns an error, an empty response, or a 403 is almost always one of these rather than your Dart. Rule them out before you debug code.
Billing. Image generation has no free tier. On a Spark-plan project the image models return
limit: 0
for
generate_content_free_tier_requests
, so the first request fails on quota having made zero requests. Text models do work on Spark, which means "my other Gemini call works" proves nothing. Upgrade to Blaze, then verify the current limits rather than trusting this note:
bash
gcloud services quota list --service=generativelanguage.googleapis.com --consumer=projects/YOUR_PROJECT_ID
App Check. Firebase AI Logic enforces App Check when the project has it turned on. Otherwise the endpoint is open to anyone who extracts your config from the shipped client, and that config is public by design. Anything reachable from a device you do not control needs App Check. Debug builds attest with a debug provider, release builds with a real one. Web has a specific trap that costs an afternoon, described in
references/setup.md
.
responseModalities
.
Without it the model has no permission to return an image, so you get text describing the picture it would have drawn. Set both modalities, as in the call below.
首次调用返回错误、空响应或403状态码,几乎都是由以下问题导致,而非你的Dart代码有问题。在调试代码前请先排查这些因素。
计费问题。 图像生成没有免费额度。在Spark方案的项目中,图像模型的
generate_content_free_tier_requests
会返回
limit: 0
,因此即使从未发起过请求,首次调用也会因配额不足失败。文本模型在Spark方案下可以正常使用,因此“我的其他Gemini调用正常”并不能说明问题。请升级到Blaze方案,然后验证当前配额限制,不要仅依赖本文说明:
bash
gcloud services quota list --service=generativelanguage.googleapis.com --consumer=projects/YOUR_PROJECT_ID
App Check问题。 如果项目开启了App Check,Firebase AI Logic会强制校验。否则,任何从发布的客户端中提取到配置的人都可以访问该接口,而该配置本身就是设计为公开的。所有从不受你控制的设备访问的服务都需要启用App Check。调试构建版本使用调试提供方进行认证,正式构建版本使用正式提供方。Web端有一个容易耗费一下午时间的典型坑,在
references/setup.md
中有说明。
responseModalities
配置问题。
不设置该参数的话,模型没有返回图像的权限,因此你只会收到描述它本该生成的图像的文本。请同时设置两种输出模式,如下方调用示例所示。

2. The minimal call that works

2. 最小可用调用示例

dart
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()
is the Gemini Developer API. Prefer it: it needs no GCP surface of its own, and its free tier covers text.
FirebaseAI.vertexAI()
requires Blaze regardless of model and buys you GCP-side controls, so reach for it when the project already lives in Vertex rather than by default.
Do not pass
appCheck:
or
auth:
to
googleAI()
. Both parameters are deprecated in current
firebase_ai
; the instance resolves them from the
FirebaseApp
on its own.
For model IDs, aspect-ratio and size enums, and when Imagen beats Gemini, read
references/models.md
.
dart
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 AI再选择它,不要默认使用。
不要向
googleAI()
传递
appCheck:
或
auth:
参数。这两个参数在当前版本的
firebase_ai
中已被弃用;实例会自动从
FirebaseApp
中解析这些配置。
关于模型ID、宽高比和尺寸枚举,以及Imagen优于Gemini的场景,请阅读
references/models.md
。

3. Reading the response

3. 处理响应内容

The convenience
.text
accessor does not capture the full sequence, so walk the parts directly. Pattern-match on the part type, which keeps the switch correct when the SDK adds new part types:
dart
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;
  }
}
Keep the interpretation of a response in its own pure function taking a
Candidate
.
Candidate
,
Content
,
TextPart
and
InlineDataPart
are all publicly constructible, so that function is testable with real SDK types, no Firebase and no test doubles. It is the one seam in this stack that unit tests genuinely reach.
便捷的
.text
访问器无法获取完整的响应序列,因此请直接遍历所有片段。对片段类型进行模式匹配,这样当SDK新增片段类型时,switch语句依然能正确处理:
dart
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
都支持公开构造,因此该函数可以使用真实的SDK类型进行测试,不需要依赖Firebase,也不需要测试替身。这是整个技术栈中单元测试真正能够覆盖的边界。

When no image comes back

未返回图像的情况

An empty result surfaces as a blank error and reads like a client bug, which sends people debugging the wrong half of the system. The response carries the reason, so report it. In order:
CheckWhereMeans
response.promptFeedback?.blockReason
before candidatesYour input was rejected. Read
blockReasonMessage
too.
response.candidates
empty
n/aNothing generated at all.
candidate.finishReason
on the candidateThe model stopped: safety, recitation, or a token limit.
finishMessage
adds detail.
No image but text presentafter walking partsIt answered in prose instead of drawing. Usually a prompt problem.
Everything empty, no reasonn/aSay so plainly and let the user retry. This happens intermittently.
Image-only responses, with no text at all, also happen intermittently on prompts that reliably return both. If you ask for text alongside the image, treat its absence as normal and degrade instead of throwing.
空结果通常表现为无明确信息的错误,看起来像是客户端bug,会导致开发者去排查错误的方向。响应中包含了失败原因,请按以下顺序排查:
检查项位置含义
response.promptFeedback?.blockReason
candidates之前你的输入被拒绝。请同时查看
blockReasonMessage
。
response.candidates
为空
无完全没有生成任何内容。
candidate.finishReason
candidate对象上模型停止生成:原因可能是安全策略、复述限制或token上限。
finishMessage
会提供详细信息。
无图像但有文本遍历片段后模型用文字回答而非生成图像,通常是提示词问题。
全部为空且无原因无直接告知用户该情况并让其重试,这种问题会间歇性出现。
对于原本会同时返回文本和图像的提示词,偶尔也会出现仅返回图像、完全没有文本的情况。如果你要求同时返回文本和图像,请将文本缺失视为正常情况,做降级处理而非抛出异常。

4. Sending a user photo

4. 传入用户照片

Downscale before you send.
InlineDataPart.toJson()
base64-encodes the bytes synchronously on the main isolate, so a full-size phone photo freezes the UI for seconds while the request is built. Gemini downsamples large images anyway, so you pay for detail that is then discarded. Scale at pick time rather than after:
dart
final file = await picker.pickImage(
  source: ImageSource.gallery,
  maxWidth: 1280,
  maxHeight: 1280,
  imageQuality: 85,
);
Size the cap to your subject rather than to a habit. 1280px on the long edge holds a face or a single figure comfortably, while fine texture, legible text in the source, or a wide scene the model has to read across will want more. Match
mimeType
to what the picker actually returned.
发送前先缩小图片尺寸。
InlineDataPart.toJson()
会在主隔离(main isolate)中同步对字节进行base64编码,因此全尺寸的手机照片会在构建请求时导致UI卡顿数秒。况且Gemini本身会对大图片进行下采样,因此你为不会被用到的细节付出了不必要的代价。请在选择图片时就调整尺寸,而非之后再处理:
dart
final file = await picker.pickImage(
  source: ImageSource.gallery,
  maxWidth: 1280,
  maxHeight: 1280,
  imageQuality: 85,
);
请根据你的拍摄对象设置尺寸上限,而非凭习惯固定值。长边1280px足以清晰呈现人脸或单个人物,但如果需要识别精细纹理、源图中清晰的文字,或是模型需要解读的宽幅场景,则需要更高的分辨率。请将
mimeType
设置为与选择器实际返回的类型一致。

5. Sizing the result

5. 调整输出结果尺寸

Use
ImageConfig
when your ratio is one of the supported enum values, because it is a real constraint. Asking for a ratio in the prompt text is a suggestion the model frequently ignores. If you need a ratio the enum does not offer, 2:1 for instance, measure what came back and lay out from the measurement:
dart
final descriptor = await ui.ImageDescriptor.encoded(
  await ui.ImmutableBuffer.fromUint8List(bytes),
);
final size = ui.Size(descriptor.width.toDouble(), descriptor.height.toDouble());
descriptor.dispose();
Wrap it so a failure returns null instead of throwing. An image you cannot measure is still an image worth showing.
如果你需要的宽高比在支持的枚举值范围内,请使用
ImageConfig
,因为这是硬性约束。在提示词文本中要求宽高比只是建议,模型经常会忽略。如果你需要枚举不支持的比例(比如2:1),请先测量返回图像的尺寸,再根据测量结果进行布局:
dart
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. Getting text and image from one call

6. 单次调用同时获取文本和图像

You often want machine-readable data about the image the model just drew, such as a caption or the names it invented. Once that data is pixels your app cannot read it, so ask for it as text in the same call and reconcile the two halves in the prompt: "the title painted into the image must match the JSON character for character."
Parse that text defensively. Asked for a fenced ```json block, the model will across one session return fenced JSON, bare JSON, prose-wrapped JSON, and nothing at all. Try the fence, fall back to the first balanced
{...}
, and return null instead of throwing.
你通常需要获取模型生成图像的机器可读数据,比如标题或它生成的名称。一旦这些数据变成像素,你的应用就无法读取了,因此请在同一次调用中要求以文本形式返回这些数据,并在提示词中明确两者的一致性:“绘制在图像中的标题必须与JSON中的内容逐字符完全一致”。
请以防御性的方式解析该文本。即使要求模型返回用```包裹的json块,在同一会话中,模型也可能返回带包裹的JSON、裸JSON、被散文包裹的JSON,甚至什么都不返回。请先尝试解析包裹的JSON,失败则回退到查找第一个配对的
{...}
,解析失败时返回null而非抛出异常。

7. Prompting

7. 提示词编写

The failures here are not code failures, and no unit test reaches them. The recurring ones have specific fixes worth knowing before you write the first prompt: placeholder words painted literally into the artwork, the source photo's clothing surviving an outfit change, text spelled differently in two places, and panels that do not share a background. Read
references/prompting.md
.
这里的失败不是代码问题,单元测试也无法覆盖。在编写第一个提示词之前,有必要了解一些常见问题的特定解决方法:占位词被直接绘制到图像中、源照片的衣服在换装后依然保留、两处文本拼写不一致、多面板图像背景不统一等。请阅读
references/prompting.md
。

8. Deciding what to test

8. 确定测试范围

The seam is at your boundary. Unit tests protect your interpretation of a response and nothing past it, so a green suite says nothing about whether the app produces a good image.
references/testing.md
covers the layers and what each one cannot reach.
测试边界在你的代码边界处。单元测试只能覆盖你对响应的解析逻辑,无法覆盖更外层的内容,因此测试全绿并不能保证应用生成的图像质量达标。
references/testing.md
介绍了不同的测试层级以及各自无法覆盖的范围。

External documentation

外部文档

Everything here is a summary that will drift. When a detail matters, confirm it at the source.
本文所有内容都是总结,可能会随时间过时。如果涉及关键细节,请以官方源文档为准。

Reference files

参考文件

  • references/setup.md
    : Firebase console path, provider choice, billing, and the web App Check debug-token trap
  • references/models.md
    : model IDs, aspect-ratio and size enums, Gemini vs Imagen
  • references/prompting.md
    : the mistakes image prompts actually make, and the phrasings that fix them
  • references/testing.md
    : unit, golden, e2e and eval layers, and what each cannot reach
  • references/setup.md
    :Firebase控制台路径、提供方选择、计费,以及Web端App Check调试令牌的坑
  • references/models.md
    :模型ID、宽高比和尺寸枚举、Gemini与Imagen的对比
  • references/prompting.md
    :图像提示词的常见错误及修正措辞
  • references/testing.md
    :单元测试、快照测试、E2E测试和评估层级,以及各自的覆盖局限