Skip to content

Commit cd9d896

Browse files
feat(vertexai): add Imagen support (#16976)
* first pass of imagen * working demo * Separate generateImages and generateImagesGCS * expose ImagenImage basetype for future * updates after api review * organize example page and functions * fix analyzer * imagen model working with new example layout * Finish up all functionality * remove the storage uri prompt page after merge * Add example for gcs generation * Add ImagenImagesBlockedException * Add unit tests * update the year * finalize with the name * Hide gcs api and some review feedback * Update packages/firebase_vertexai/firebase_vertexai/lib/src/imagen_api.dart Co-authored-by: Nate Bosch <[email protected]> * Update packages/firebase_vertexai/firebase_vertexai/lib/src/imagen_model.dart Co-authored-by: Nate Bosch <[email protected]> * review comments * More review comment update * fix analyzer --------- Co-authored-by: Nate Bosch <[email protected]>
1 parent 56314d7 commit cd9d896

22 files changed

+1271
-312
lines changed

packages/firebase_vertexai/firebase_vertexai/example/ios/Runner/AppDelegate.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import UIKit
22
import Flutter
33

4-
@UIApplicationMain
4+
@main
55
@objc class AppDelegate: FlutterAppDelegate {
66
override func application(
77
_ application: UIApplication,

packages/firebase_vertexai/firebase_vertexai/example/lib/main.dart

+5-5
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import 'pages/function_calling_page.dart';
2222
import 'pages/image_prompt_page.dart';
2323
import 'pages/token_count_page.dart';
2424
import 'pages/schema_page.dart';
25-
import 'pages/storage_uri_page.dart';
25+
import 'pages/imagen_page.dart';
2626

2727
// REQUIRED if you want to run on Web
2828
const FirebaseOptions? options = null;
@@ -79,7 +79,7 @@ class _HomeScreenState extends State<HomeScreen> {
7979
title: 'Function Calling',
8080
), // function calling will initial its own model
8181
ImagePromptPage(title: 'Image Prompt', model: widget.model),
82-
StorageUriPromptPage(title: 'Storage URI Prompt', model: widget.model),
82+
ImagenPage(title: 'Imagen Model', model: widget.model),
8383
SchemaPromptPage(title: 'Schema Prompt', model: widget.model),
8484
];
8585

@@ -134,11 +134,11 @@ class _HomeScreenState extends State<HomeScreen> {
134134
),
135135
BottomNavigationBarItem(
136136
icon: Icon(
137-
Icons.folder,
137+
Icons.image_search,
138138
color: Theme.of(context).colorScheme.primary,
139139
),
140-
label: 'Storage URI Prompt',
141-
tooltip: 'Storage URI Prompt',
140+
label: 'Imagen Model',
141+
tooltip: 'Imagen Model',
142142
),
143143
BottomNavigationBarItem(
144144
icon: Icon(

packages/firebase_vertexai/firebase_vertexai/example/lib/pages/image_prompt_page.dart

+65-8
Original file line numberDiff line numberDiff line change
@@ -89,14 +89,28 @@ class _ImagePromptPageState extends State<ImagePromptPage> {
8989
const SizedBox.square(
9090
dimension: 15,
9191
),
92-
ElevatedButton(
93-
onPressed: !_loading
94-
? () async {
95-
await _sendImagePrompt(_textController.text);
96-
}
97-
: null,
98-
child: const Text('Send Image Prompt'),
99-
),
92+
if (!_loading)
93+
IconButton(
94+
onPressed: () async {
95+
await _sendImagePrompt(_textController.text);
96+
},
97+
icon: Icon(
98+
Icons.image,
99+
color: Theme.of(context).colorScheme.primary,
100+
),
101+
),
102+
if (!_loading)
103+
IconButton(
104+
onPressed: () async {
105+
await _sendStorageUriPrompt(_textController.text);
106+
},
107+
icon: Icon(
108+
Icons.storage,
109+
color: Theme.of(context).colorScheme.primary,
110+
),
111+
)
112+
else
113+
const CircularProgressIndicator(),
100114
],
101115
),
102116
),
@@ -162,6 +176,49 @@ class _ImagePromptPageState extends State<ImagePromptPage> {
162176
}
163177
}
164178

179+
Future<void> _sendStorageUriPrompt(String message) async {
180+
setState(() {
181+
_loading = true;
182+
});
183+
try {
184+
final content = [
185+
Content.multi([
186+
TextPart(message),
187+
FileData(
188+
'image/jpeg',
189+
'gs://vertex-ai-example-ef5a2.appspot.com/foodpic.jpg',
190+
),
191+
]),
192+
];
193+
_generatedContent.add(MessageData(text: message, fromUser: true));
194+
195+
var response = await widget.model.generateContent(content);
196+
var text = response.text;
197+
_generatedContent.add(MessageData(text: text, fromUser: false));
198+
199+
if (text == null) {
200+
_showError('No response from API.');
201+
return;
202+
} else {
203+
setState(() {
204+
_loading = false;
205+
_scrollDown();
206+
});
207+
}
208+
} catch (e) {
209+
_showError(e.toString());
210+
setState(() {
211+
_loading = false;
212+
});
213+
} finally {
214+
_textController.clear();
215+
setState(() {
216+
_loading = false;
217+
});
218+
_textFieldFocus.requestFocus();
219+
}
220+
}
221+
165222
void _showError(String message) {
166223
showDialog<void>(
167224
context: context,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import 'package:flutter/material.dart';
16+
import 'package:firebase_vertexai/firebase_vertexai.dart';
17+
//import 'package:firebase_storage/firebase_storage.dart';
18+
import '../widgets/message_widget.dart';
19+
20+
class ImagenPage extends StatefulWidget {
21+
const ImagenPage({
22+
super.key,
23+
required this.title,
24+
required this.model,
25+
});
26+
27+
final String title;
28+
final GenerativeModel model;
29+
30+
@override
31+
State<ImagenPage> createState() => _ImagenPageState();
32+
}
33+
34+
class _ImagenPageState extends State<ImagenPage> {
35+
final ScrollController _scrollController = ScrollController();
36+
final TextEditingController _textController = TextEditingController();
37+
final FocusNode _textFieldFocus = FocusNode();
38+
final List<MessageData> _generatedContent = <MessageData>[];
39+
bool _loading = false;
40+
late final ImagenModel _imagenModel;
41+
42+
@override
43+
void initState() {
44+
super.initState();
45+
var generationConfig = ImagenGenerationConfig(
46+
negativePrompt: 'frog',
47+
numberOfImages: 1,
48+
aspectRatio: ImagenAspectRatio.square1x1,
49+
imageFormat: ImagenFormat.jpeg(compressionQuality: 75),
50+
);
51+
_imagenModel = FirebaseVertexAI.instance.imagenModel(
52+
model: 'imagen-3.0-generate-001',
53+
generationConfig: generationConfig,
54+
safetySettings: ImagenSafetySettings(
55+
ImagenSafetyFilterLevel.blockLowAndAbove,
56+
ImagenPersonFilterLevel.allowAdult,
57+
),
58+
);
59+
}
60+
61+
void _scrollDown() {
62+
WidgetsBinding.instance.addPostFrameCallback(
63+
(_) => _scrollController.animateTo(
64+
_scrollController.position.maxScrollExtent,
65+
duration: const Duration(
66+
milliseconds: 750,
67+
),
68+
curve: Curves.easeOutCirc,
69+
),
70+
);
71+
}
72+
73+
@override
74+
Widget build(BuildContext context) {
75+
return Scaffold(
76+
appBar: AppBar(
77+
title: Text(widget.title),
78+
),
79+
body: Padding(
80+
padding: const EdgeInsets.all(8),
81+
child: Column(
82+
mainAxisAlignment: MainAxisAlignment.center,
83+
crossAxisAlignment: CrossAxisAlignment.start,
84+
children: [
85+
Expanded(
86+
child: ListView.builder(
87+
controller: _scrollController,
88+
itemBuilder: (context, idx) {
89+
return MessageWidget(
90+
text: _generatedContent[idx].text,
91+
image: _generatedContent[idx].image,
92+
isFromUser: _generatedContent[idx].fromUser ?? false,
93+
);
94+
},
95+
itemCount: _generatedContent.length,
96+
),
97+
),
98+
Padding(
99+
padding: const EdgeInsets.symmetric(
100+
vertical: 25,
101+
horizontal: 15,
102+
),
103+
child: Row(
104+
children: [
105+
Expanded(
106+
child: TextField(
107+
autofocus: true,
108+
focusNode: _textFieldFocus,
109+
controller: _textController,
110+
),
111+
),
112+
const SizedBox.square(
113+
dimension: 15,
114+
),
115+
if (!_loading)
116+
IconButton(
117+
onPressed: () async {
118+
await _testImagen(_textController.text);
119+
},
120+
icon: Icon(
121+
Icons.image_search,
122+
color: Theme.of(context).colorScheme.primary,
123+
),
124+
tooltip: 'Imagen raw data',
125+
)
126+
else
127+
const CircularProgressIndicator(),
128+
// NOTE: Keep this API private until future release.
129+
// if (!_loading)
130+
// IconButton(
131+
// onPressed: () async {
132+
// await _testImagenGCS(_textController.text);
133+
// },
134+
// icon: Icon(
135+
// Icons.imagesearch_roller,
136+
// color: Theme.of(context).colorScheme.primary,
137+
// ),
138+
// tooltip: 'Imagen GCS',
139+
// )
140+
// else
141+
// const CircularProgressIndicator(),
142+
],
143+
),
144+
),
145+
],
146+
),
147+
),
148+
);
149+
}
150+
151+
Future<void> _testImagen(String prompt) async {
152+
setState(() {
153+
_loading = true;
154+
});
155+
156+
var response = await _imagenModel.generateImages(prompt);
157+
158+
if (response.images.isNotEmpty) {
159+
var imagenImage = response.images[0];
160+
// Process the image
161+
_generatedContent.add(
162+
MessageData(
163+
image: Image.memory(imagenImage.bytesBase64Encoded),
164+
text: prompt,
165+
fromUser: false,
166+
),
167+
);
168+
} else {
169+
// Handle the case where no images were generated
170+
_showError('Error: No images were generated.');
171+
}
172+
setState(() {
173+
_loading = false;
174+
_scrollDown();
175+
});
176+
}
177+
// NOTE: Keep this API private until future release.
178+
// Future<void> _testImagenGCS(String prompt) async {
179+
// setState(() {
180+
// _loading = true;
181+
// });
182+
// var gcsUrl = 'gs://vertex-ai-example-ef5a2.appspot.com/imagen';
183+
184+
// var response = await _imagenModel.generateImagesGCS(prompt, gcsUrl);
185+
186+
// if (response.images.isNotEmpty) {
187+
// var imagenImage = response.images[0];
188+
// final returnImageUri = imagenImage.gcsUri;
189+
// final reference = FirebaseStorage.instance.refFromURL(returnImageUri);
190+
// final downloadUrl = await reference.getDownloadURL();
191+
// // Process the image
192+
// _generatedContent.add(
193+
// MessageData(
194+
// image: Image(image: NetworkImage(downloadUrl)),
195+
// text: prompt,
196+
// fromUser: false,
197+
// ),
198+
// );
199+
// } else {
200+
// // Handle the case where no images were generated
201+
// _showError('Error: No images were generated.');
202+
// }
203+
// setState(() {
204+
// _loading = false;
205+
// });
206+
// }
207+
208+
void _showError(String message) {
209+
showDialog<void>(
210+
context: context,
211+
builder: (context) {
212+
return AlertDialog(
213+
title: const Text('Something went wrong'),
214+
content: SingleChildScrollView(
215+
child: SelectableText(message),
216+
),
217+
actions: [
218+
TextButton(
219+
onPressed: () {
220+
Navigator.of(context).pop();
221+
},
222+
child: const Text('OK'),
223+
),
224+
],
225+
);
226+
},
227+
);
228+
}
229+
}

0 commit comments

Comments
 (0)