Skip to content

Commit 5e1c3b4

Browse files
authored
[AI Logic] Add Imagen support (#1279)
* [AI Logic] Add Imagen support * Address gemini feedback * Update UIHandlerAutomated.cs * Update readme.md
1 parent 1172af2 commit 5e1c3b4

File tree

15 files changed

+935
-63
lines changed

15 files changed

+935
-63
lines changed

docs/readme.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,8 @@ Release Notes
115115
- Analytics: Removed deprecated `FirebaseAnalytics.ParameterGroupId`
116116
and `Parameter.Dispose` methods.
117117
- Auth: Removed deprecated `FirebaseUser.UpdateEmailAsync`.
118+
- Firebase AI: Add support for image generation via Imagen. For more info, see
119+
https://firebase.google.com/docs/ai-logic/generate-images-imagen
118120
- Firebase AI: Deprecated `CountTokensResponse.TotalBillableCharacters`, use
119121
`CountTokensResponse.TotalTokens` instead.
120122
- Messaging: Removed deprecated `FirebaseMessage.Dispose`,

firebaseai/src/FirebaseAI.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,28 @@ public LiveGenerativeModel GetLiveModel(
191191
liveGenerationConfig, tools,
192192
systemInstruction, requestOptions);
193193
}
194+
195+
/// <summary>
196+
/// Initializes an `ImagenModel` with the given parameters.
197+
///
198+
/// - Important: Only Imagen 3 models (named `imagen-3.0-*`) are supported.
199+
/// </summary>
200+
/// <param name="modelName">The name of the Imagen 3 model to use, for example `"imagen-3.0-generate-002"`;
201+
/// see [model versions](https://firebase.google.com/docs/vertex-ai/models) for a list of
202+
/// supported Imagen 3 models.</param>
203+
/// <param name="generationConfig">Configuration options for generating images with Imagen.</param>
204+
/// <param name="safetySettings">Settings describing what types of potentially harmful content your model
205+
/// should allow.</param>
206+
/// <param name="requestOptions">Configuration parameters for sending requests to the backend.</param>
207+
/// <returns>The initialized `ImagenModel` instance.</returns>
208+
public ImagenModel GetImagenModel(
209+
string modelName,
210+
ImagenGenerationConfig? generationConfig = null,
211+
ImagenSafetySettings? safetySettings = null,
212+
RequestOptions? requestOptions = null) {
213+
return new ImagenModel(_firebaseApp, _backend, modelName,
214+
generationConfig, safetySettings, requestOptions);
215+
}
194216
}
195217

196218
}

firebaseai/src/GenerativeModel.cs

Lines changed: 12 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -200,10 +200,11 @@ public Chat StartChat(IEnumerable<ModelContent> history) {
200200
private async Task<GenerateContentResponse> GenerateContentAsyncInternal(
201201
IEnumerable<ModelContent> content,
202202
CancellationToken cancellationToken) {
203-
HttpRequestMessage request = new(HttpMethod.Post, GetURL() + ":generateContent");
203+
HttpRequestMessage request = new(HttpMethod.Post,
204+
HttpHelpers.GetURL(_firebaseApp, _backend, _modelName) + ":generateContent");
204205

205206
// Set the request headers
206-
await SetRequestHeaders(request);
207+
await HttpHelpers.SetRequestHeaders(request, _firebaseApp);
207208

208209
// Set the content
209210
string bodyJson = MakeGenerateContentRequest(content);
@@ -214,7 +215,7 @@ private async Task<GenerateContentResponse> GenerateContentAsyncInternal(
214215
#endif
215216

216217
var response = await _httpClient.SendAsync(request, cancellationToken);
217-
await ValidateHttpResponse(response);
218+
await HttpHelpers.ValidateHttpResponse(response);
218219

219220
string result = await response.Content.ReadAsStringAsync();
220221

@@ -225,40 +226,14 @@ private async Task<GenerateContentResponse> GenerateContentAsyncInternal(
225226
return GenerateContentResponse.FromJson(result, _backend.Provider);
226227
}
227228

228-
// Helper function to throw an exception if the Http Response indicates failure.
229-
// Useful as EnsureSuccessStatusCode can leave out relevant information.
230-
private async Task ValidateHttpResponse(HttpResponseMessage response) {
231-
if (response.IsSuccessStatusCode) {
232-
return;
233-
}
234-
235-
// Status code indicates failure, try to read the content for more details
236-
string errorContent = "No error content available.";
237-
if (response.Content != null) {
238-
try {
239-
errorContent = await response.Content.ReadAsStringAsync();
240-
} catch (Exception readEx) {
241-
// Handle being unable to read the content
242-
errorContent = $"Failed to read error content: {readEx.Message}";
243-
}
244-
}
245-
246-
// Construct the exception with as much information as possible.
247-
var ex = new HttpRequestException(
248-
$"HTTP request failed with status code: {(int)response.StatusCode} ({response.ReasonPhrase}).\n" +
249-
$"Error Content: {errorContent}"
250-
);
251-
252-
throw ex;
253-
}
254-
255229
private async IAsyncEnumerable<GenerateContentResponse> GenerateContentStreamAsyncInternal(
256230
IEnumerable<ModelContent> content,
257231
[EnumeratorCancellation] CancellationToken cancellationToken) {
258-
HttpRequestMessage request = new(HttpMethod.Post, GetURL() + ":streamGenerateContent?alt=sse");
232+
HttpRequestMessage request = new(HttpMethod.Post,
233+
HttpHelpers.GetURL(_firebaseApp, _backend, _modelName) + ":streamGenerateContent?alt=sse");
259234

260235
// Set the request headers
261-
await SetRequestHeaders(request);
236+
await HttpHelpers.SetRequestHeaders(request, _firebaseApp);
262237

263238
// Set the content
264239
string bodyJson = MakeGenerateContentRequest(content);
@@ -269,7 +244,7 @@ private async IAsyncEnumerable<GenerateContentResponse> GenerateContentStreamAsy
269244
#endif
270245

271246
var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
272-
await ValidateHttpResponse(response);
247+
await HttpHelpers.ValidateHttpResponse(response);
273248

274249
// We are expecting a Stream as the response, so handle that.
275250
using var stream = await response.Content.ReadAsStreamAsync();
@@ -291,10 +266,11 @@ private async IAsyncEnumerable<GenerateContentResponse> GenerateContentStreamAsy
291266
private async Task<CountTokensResponse> CountTokensAsyncInternal(
292267
IEnumerable<ModelContent> content,
293268
CancellationToken cancellationToken) {
294-
HttpRequestMessage request = new(HttpMethod.Post, GetURL() + ":countTokens");
269+
HttpRequestMessage request = new(HttpMethod.Post,
270+
HttpHelpers.GetURL(_firebaseApp, _backend, _modelName) + ":countTokens");
295271

296272
// Set the request headers
297-
await SetRequestHeaders(request);
273+
await HttpHelpers.SetRequestHeaders(request, _firebaseApp);
298274

299275
// Set the content
300276
string bodyJson = MakeCountTokensRequest(content);
@@ -305,7 +281,7 @@ private async Task<CountTokensResponse> CountTokensAsyncInternal(
305281
#endif
306282

307283
var response = await _httpClient.SendAsync(request, cancellationToken);
308-
await ValidateHttpResponse(response);
284+
await HttpHelpers.ValidateHttpResponse(response);
309285

310286
string result = await response.Content.ReadAsStringAsync();
311287

@@ -316,33 +292,6 @@ private async Task<CountTokensResponse> CountTokensAsyncInternal(
316292
return CountTokensResponse.FromJson(result);
317293
}
318294

319-
private string GetURL() {
320-
if (_backend.Provider == FirebaseAI.Backend.InternalProvider.VertexAI) {
321-
return "https://firebasevertexai.googleapis.com/v1beta" +
322-
"/projects/" + _firebaseApp.Options.ProjectId +
323-
"/locations/" + _backend.Location +
324-
"/publishers/google/models/" + _modelName;
325-
} else if (_backend.Provider == FirebaseAI.Backend.InternalProvider.GoogleAI) {
326-
return "https://firebasevertexai.googleapis.com/v1beta" +
327-
"/projects/" + _firebaseApp.Options.ProjectId +
328-
"/models/" + _modelName;
329-
} else {
330-
throw new NotSupportedException($"Missing support for backend: {_backend.Provider}");
331-
}
332-
}
333-
334-
private async Task SetRequestHeaders(HttpRequestMessage request) {
335-
request.Headers.Add("x-goog-api-key", _firebaseApp.Options.ApiKey);
336-
string version = FirebaseInterops.GetVersionInfoSdkVersion();
337-
request.Headers.Add("x-goog-api-client", $"gl-csharp/8.0 fire/{version}");
338-
if (FirebaseInterops.GetIsDataCollectionDefaultEnabled(_firebaseApp)) {
339-
request.Headers.Add("X-Firebase-AppId", _firebaseApp.Options.AppId);
340-
request.Headers.Add("X-Firebase-AppVersion", UnityEngine.Application.version);
341-
}
342-
// Add additional Firebase tokens to the header.
343-
await FirebaseInterops.AddFirebaseTokensAsync(request, _firebaseApp);
344-
}
345-
346295
private string MakeGenerateContentRequest(IEnumerable<ModelContent> contents) {
347296
Dictionary<string, object> jsonDict = MakeGenerateContentRequestAsDictionary(contents);
348297
return Json.Serialize(jsonDict);

firebaseai/src/Imagen.meta

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

firebaseai/src/Imagen/ImagenConfig.cs

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
using System.Collections.Generic;
18+
19+
namespace Firebase.AI {
20+
/// <summary>
21+
/// An aspect ratio for images generated by Imagen.
22+
///
23+
/// To specify an aspect ratio for generated images, set `AspectRatio` in
24+
/// your `ImagenGenerationConfig`. See the [Cloud
25+
/// documentation](https://cloud.google.com/vertex-ai/generative-ai/docs/image/generate-images#aspect-ratio)
26+
/// for more details and examples of the supported aspect ratios.
27+
/// </summary>
28+
public enum ImagenAspectRatio {
29+
/// <summary>
30+
/// Square (1:1) aspect ratio.
31+
///
32+
/// Common uses for this aspect ratio include social media posts.
33+
/// </summary>
34+
Square1x1,
35+
/// <summary>
36+
/// Portrait widescreen (9:16) aspect ratio.
37+
///
38+
/// This is the `Landscape16x9` aspect ratio rotated 90 degrees. This a relatively new aspect
39+
/// ratio that has been popularized by short form video apps (for example, YouTube shorts). Use
40+
/// this for tall objects with strong vertical orientations such as buildings, trees, waterfalls,
41+
/// or other similar objects.
42+
/// </summary>
43+
Portrait9x16,
44+
/// <summary>
45+
/// Widescreen (16:9) aspect ratio.
46+
///
47+
/// This ratio has replaced `Landscape4x3` as the most common aspect ratio for TVs, monitors,
48+
/// and mobile phone screens (landscape). Use this aspect ratio when you want to capture more of
49+
/// the background (for example, scenic landscapes).
50+
/// </summary>
51+
Landscape16x9,
52+
/// <summary>
53+
/// Portrait full screen (3:4) aspect ratio.
54+
///
55+
/// This is the `Landscape4x3` aspect ratio rotated 90 degrees. This lets to capture more of
56+
/// the scene vertically compared to the `Square1x1` aspect ratio.
57+
/// </summary>
58+
Portrait3x4,
59+
/// <summary>
60+
/// Fullscreen (4:3) aspect ratio.
61+
///
62+
/// This aspect ratio is commonly used in media or film. It is also the dimensions of most old
63+
/// (non-widescreen) TVs and medium format cameras. It captures more of the scene horizontally
64+
/// (compared to `Square1x1`), making it a preferred aspect ratio for photography.
65+
/// </summary>
66+
Landscape4x3
67+
}
68+
69+
/// <summary>
70+
/// An image format for images generated by Imagen.
71+
///
72+
/// To specify an image format for generated images, set `ImageFormat` in
73+
/// your `ImagenGenerationConfig`. See the [Cloud
74+
/// documentation](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/imagen-api#output-options)
75+
/// for more details.
76+
/// </summary>
77+
public readonly struct ImagenImageFormat {
78+
#if !DOXYGEN
79+
public string MimeType { get; }
80+
public int? CompressionQuality { get; }
81+
#endif
82+
83+
private ImagenImageFormat(string mimeType, int? compressionQuality = null) {
84+
MimeType = mimeType;
85+
CompressionQuality = compressionQuality;
86+
}
87+
88+
/// <summary>
89+
/// PNG image format.
90+
///
91+
/// Portable Network Graphic (PNG) is a lossless image format, meaning no image data is lost
92+
/// during compression. Images in PNG format are *typically* larger than JPEG images, though this
93+
/// depends on the image content and JPEG compression quality.
94+
/// </summary>
95+
public static ImagenImageFormat Png() {
96+
return new ImagenImageFormat("image/png");
97+
}
98+
99+
/// <summary>
100+
/// JPEG image format.
101+
///
102+
/// Joint Photographic Experts Group (JPEG) is a lossy compression format, meaning some image data
103+
/// is discarded during compression. Images in JPEG format are *typically* larger than PNG images,
104+
/// though this depends on the image content and JPEG compression quality.
105+
/// </summary>
106+
/// <param name="compressionQuality">The JPEG quality setting from 0 to 100, where `0` is highest level of
107+
/// compression (lowest image quality, smallest file size) and `100` is the lowest level of
108+
/// compression (highest image quality, largest file size); defaults to `75`.</param>
109+
public static ImagenImageFormat Jpeg(int? compressionQuality = null) {
110+
return new ImagenImageFormat("image/jpeg", compressionQuality);
111+
}
112+
113+
/// <summary>
114+
/// Intended for internal use only.
115+
/// This method is used for serializing the object to JSON for the API request.
116+
/// </summary>
117+
internal Dictionary<string, object> ToJson() {
118+
Dictionary<string, object> jsonDict = new() {
119+
["mimeType"] = MimeType
120+
};
121+
if (CompressionQuality != null) {
122+
jsonDict["compressionQuality"] = CompressionQuality.Value;
123+
}
124+
return jsonDict;
125+
}
126+
}
127+
128+
/// <summary>
129+
/// Configuration options for generating images with Imagen.
130+
///
131+
/// See [Parameters for Imagen
132+
/// models](https://firebase.google.com/docs/vertex-ai/model-parameters?platform=unity#imagen) to
133+
/// learn about parameters available for use with Imagen models, including how to configure them.
134+
/// </summary>
135+
public readonly struct ImagenGenerationConfig {
136+
#if !DOXYGEN
137+
public string NegativePrompt { get; }
138+
public int? NumberOfImages { get; }
139+
public ImagenAspectRatio? AspectRatio { get; }
140+
public ImagenImageFormat? ImageFormat { get; }
141+
public bool? AddWatermark { get; }
142+
#endif
143+
144+
/// <summary>
145+
/// Initializes configuration options for generating images with Imagen.
146+
/// </summary>
147+
/// <param name="negativePrompt">Specifies elements to exclude from the generated image;
148+
/// disabled if not specified.</param>
149+
/// <param name="numberOfImages">The number of image samples to generate;
150+
/// defaults to 1 if not specified.</param>
151+
/// <param name="aspectRatio">The aspect ratio of generated images;
152+
/// defaults to to square, 1:1.</param>
153+
/// <param name="imageFormat">The image format of generated images;
154+
/// defaults to PNG.</param>
155+
/// <param name="addWatermark">Whether to add an invisible watermark to generated images;
156+
/// the default value depends on the model.</param>
157+
public ImagenGenerationConfig(
158+
string negativePrompt = null,
159+
int? numberOfImages = null,
160+
ImagenAspectRatio? aspectRatio = null,
161+
ImagenImageFormat? imageFormat = null,
162+
bool? addWatermark = null) {
163+
NegativePrompt = negativePrompt;
164+
NumberOfImages = numberOfImages;
165+
AspectRatio = aspectRatio;
166+
ImageFormat = imageFormat;
167+
AddWatermark = addWatermark;
168+
}
169+
170+
private static string ConvertAspectRatio(ImagenAspectRatio aspectRatio) {
171+
return aspectRatio switch {
172+
ImagenAspectRatio.Square1x1 => "1:1",
173+
ImagenAspectRatio.Portrait9x16 => "9:16",
174+
ImagenAspectRatio.Landscape16x9 => "16:9",
175+
ImagenAspectRatio.Portrait3x4 => "3:4",
176+
ImagenAspectRatio.Landscape4x3 => "4:3",
177+
_ => aspectRatio.ToString(), // Fallback
178+
};
179+
}
180+
181+
/// <summary>
182+
/// Intended for internal use only.
183+
/// This method is used for serializing the object to JSON for the API request.
184+
/// </summary>
185+
internal Dictionary<string, object> ToJson() {
186+
Dictionary<string, object> jsonDict = new() {
187+
["sampleCount"] = NumberOfImages ?? 1
188+
};
189+
if (!string.IsNullOrEmpty(NegativePrompt)) {
190+
jsonDict["negativePrompt"] = NegativePrompt;
191+
}
192+
if (AspectRatio != null) {
193+
jsonDict["aspectRatio"] = ConvertAspectRatio(AspectRatio.Value);
194+
}
195+
if (ImageFormat != null) {
196+
jsonDict["outputOptions"] = ImageFormat?.ToJson();
197+
}
198+
if (AddWatermark != null) {
199+
jsonDict["addWatermark"] = AddWatermark;
200+
}
201+
202+
return jsonDict;
203+
}
204+
}
205+
206+
}

firebaseai/src/Imagen/ImagenConfig.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)