Skip to content

Commit aac1143

Browse files
committed
Merge branch 'main' into td/app-start
2 parents 8a6cf5a + c85227e commit aac1143

File tree

15 files changed

+293
-62
lines changed

15 files changed

+293
-62
lines changed

firebase-ai/CHANGELOG.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
# Unreleased
2+
* [fixed] Fixed an issue causing the accessor methods in `GenerateContentResponse` to throw an exception
3+
when the response contained no candidates.
24
* [changed] Added better description for requests which fail due to the Gemini API not being
35
configured.
4-
* [feature] added support for Imagen Editing, including inpainting, outpainting, control, style
6+
* [changed] Added a `dilation` parameter to `ImagenMaskReference.generateMaskAndPadForOutpainting`
7+
(#7260)
8+
9+
# 17.1.0
10+
=======
11+
* [feature] added support for Imagen Editing, including inpainting, outpainting, control, style
512
transfer, and subject references (#7075)
13+
* [feature] **Preview:** Added support for bidirectional streaming in Gemini Developer Api
614

715
# 17.0.0
816
* [feature] Added support for configuring the "thinking" budget when using Gemini
@@ -45,4 +53,3 @@
4553

4654
Note: This feature is in Public Preview, which means that it is not subject to any SLA or
4755
deprecation policy and could change in backwards-incompatible ways.
48-

firebase-ai/api.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,9 @@ package com.google.firebase.ai.java {
157157

158158
package com.google.firebase.ai.type {
159159

160+
public final class APINotConfiguredException extends com.google.firebase.ai.type.FirebaseAIException {
161+
}
162+
160163
public final class AudioRecordInitializationFailedException extends com.google.firebase.ai.type.FirebaseAIException {
161164
ctor public AudioRecordInitializationFailedException(String message);
162165
}
@@ -698,12 +701,14 @@ package com.google.firebase.ai.type {
698701
@com.google.firebase.ai.type.PublicPreviewAPI public abstract class ImagenMaskReference extends com.google.firebase.ai.type.ImagenReferenceImage {
699702
method public static final java.util.List<com.google.firebase.ai.type.ImagenReferenceImage> generateMaskAndPadForOutpainting(com.google.firebase.ai.type.ImagenInlineImage image, com.google.firebase.ai.type.Dimensions newDimensions);
700703
method public static final java.util.List<com.google.firebase.ai.type.ImagenReferenceImage> generateMaskAndPadForOutpainting(com.google.firebase.ai.type.ImagenInlineImage image, com.google.firebase.ai.type.Dimensions newDimensions, com.google.firebase.ai.type.ImagenImagePlacement newPosition = com.google.firebase.ai.type.ImagenImagePlacement.CENTER);
704+
method public static final java.util.List<com.google.firebase.ai.type.ImagenReferenceImage> generateMaskAndPadForOutpainting(com.google.firebase.ai.type.ImagenInlineImage image, com.google.firebase.ai.type.Dimensions newDimensions, com.google.firebase.ai.type.ImagenImagePlacement newPosition = com.google.firebase.ai.type.ImagenImagePlacement.CENTER, double dilation = 0.01);
701705
field public static final com.google.firebase.ai.type.ImagenMaskReference.Companion Companion;
702706
}
703707

704708
public static final class ImagenMaskReference.Companion {
705709
method public java.util.List<com.google.firebase.ai.type.ImagenReferenceImage> generateMaskAndPadForOutpainting(com.google.firebase.ai.type.ImagenInlineImage image, com.google.firebase.ai.type.Dimensions newDimensions);
706710
method public java.util.List<com.google.firebase.ai.type.ImagenReferenceImage> generateMaskAndPadForOutpainting(com.google.firebase.ai.type.ImagenInlineImage image, com.google.firebase.ai.type.Dimensions newDimensions, com.google.firebase.ai.type.ImagenImagePlacement newPosition = com.google.firebase.ai.type.ImagenImagePlacement.CENTER);
711+
method public java.util.List<com.google.firebase.ai.type.ImagenReferenceImage> generateMaskAndPadForOutpainting(com.google.firebase.ai.type.ImagenInlineImage image, com.google.firebase.ai.type.Dimensions newDimensions, com.google.firebase.ai.type.ImagenImagePlacement newPosition = com.google.firebase.ai.type.ImagenImagePlacement.CENTER, double dilation = 0.01);
707712
}
708713

709714
@com.google.firebase.ai.type.PublicPreviewAPI public final class ImagenPersonFilterLevel {

firebase-ai/gradle.properties

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,5 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
version=17.1.0
16-
latestReleasedVersion=17.0.0
15+
version=17.2.0
16+
latestReleasedVersion=17.1.0

firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Exceptions.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ public class UnsupportedUserLocationException internal constructor(cause: Throwa
158158
* [steps](https://firebase.google.com/docs/ai-logic/faq-and-troubleshooting?api=dev#error-genai-config-not-found)
159159
* to enable the Gemini Developer API.
160160
*/
161-
internal class APINotConfiguredException internal constructor(cause: Throwable? = null) :
161+
public class APINotConfiguredException internal constructor(cause: Throwable? = null) :
162162
FirebaseAIException("Gemini Developer API not enabled in Firebase console.", cause)
163163

164164
/**

firebase-ai/src/main/kotlin/com/google/firebase/ai/type/GenerateContentResponse.kt

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,28 +32,42 @@ public class GenerateContentResponse(
3232
public val usageMetadata: UsageMetadata?,
3333
) {
3434
/**
35-
* Convenience field representing all the text parts in the response as a single string, if they
36-
* exists.
35+
* Convenience field representing all the text parts in the response as a single string.
36+
*
37+
* The value is null if the response contains no [candidates].
3738
*/
3839
public val text: String? by lazy {
39-
candidates.first().content.parts.filterIsInstance<TextPart>().joinToString(" ") { it.text }
40+
candidates.firstOrNull()?.content?.parts?.filterIsInstance<TextPart>()?.joinToString(" ") {
41+
it.text
42+
}
4043
}
4144

42-
/** Convenience field to list all the [FunctionCallPart]s in the response, if they exist. */
45+
/**
46+
* Convenience field to list all the [FunctionCallPart]s in the response.
47+
*
48+
* The value is an empty list if the response contains no [candidates].
49+
*/
4350
public val functionCalls: List<FunctionCallPart> by lazy {
44-
candidates.first().content.parts.filterIsInstance<FunctionCallPart>()
51+
candidates.firstOrNull()?.content?.parts?.filterIsInstance<FunctionCallPart>().orEmpty()
4552
}
4653

4754
/**
4855
* Convenience field representing all the [InlineDataPart]s in the first candidate, if they exist.
4956
*
5057
* This also includes any [ImagePart], but they will be represented as [InlineDataPart] instead.
58+
*
59+
* The value is an empty list if the response contains no [candidates].
5160
*/
5261
public val inlineDataParts: List<InlineDataPart> by lazy {
53-
candidates.first().content.parts.let { parts ->
54-
parts.filterIsInstance<ImagePart>().map { it.toInlineDataPart() } +
55-
parts.filterIsInstance<InlineDataPart>()
56-
}
62+
candidates
63+
.firstOrNull()
64+
?.content
65+
?.parts
66+
?.let { parts ->
67+
parts.filterIsInstance<ImagePart>().map { it.toInlineDataPart() } +
68+
parts.filterIsInstance<InlineDataPart>()
69+
}
70+
.orEmpty()
5771
}
5872

5973
@Serializable

firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenControlType.kt

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,26 @@ package com.google.firebase.ai.type
1919
public class ImagenControlType internal constructor(internal val value: String) {
2020
public companion object {
2121

22-
/** Use edge detection to ensure the new image follow the same outlines */
22+
/**
23+
* Use edge detection to ensure the new image follows the same outlines as the reference image.
24+
*/
2325
@JvmField public val CANNY: ImagenControlType = ImagenControlType("CONTROL_TYPE_CANNY")
2426

25-
/** Use enhanced edge detection to ensure the new image follow similar outlines */
27+
/**
28+
* Use enhanced edge detection to ensure the new image follows the same outlines as the
29+
* reference image.
30+
*/
2631
@JvmField public val SCRIBBLE: ImagenControlType = ImagenControlType("CONTROL_TYPE_SCRIBBLE")
2732

28-
/** Use face mesh control to ensure that the new image has the same facial expressions */
33+
/**
34+
* Use face mesh control to ensure that the new image has the same facial expressions as the
35+
* reference image.
36+
*/
2937
@JvmField public val FACE_MESH: ImagenControlType = ImagenControlType("CONTROL_TYPE_FACE_MESH")
3038

3139
/**
3240
* Use color superpixels to ensure that the new image is similar in shape and color to the
33-
* original
41+
* reference image.
3442
*/
3543
@JvmField
3644
public val COLOR_SUPERPIXEL: ImagenControlType =

firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenEditMode.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public class ImagenEditMode private constructor(internal val value: String) {
2525
/** Removes an element from an image */
2626
@JvmField
2727
public val INPAINT_REMOVAL: ImagenEditMode = ImagenEditMode("EDIT_MODE_INPAINT_REMOVAL")
28-
/** Extend the borders of an image outwards */
28+
/** Extends the borders of an image outwards */
2929
@JvmField public val OUTPAINT: ImagenEditMode = ImagenEditMode("EDIT_MODE_OUTPAINT")
3030
}
3131
}

firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenReferenceImage.kt

Lines changed: 49 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,11 @@ internal constructor(
8080
* @param referenceId the reference ID for this image, to be referenced in the prompt
8181
* @param enableComputation requests that the reference image be generated serverside instead of
8282
* provided
83-
* @param superpixelRegionSize if type is COLOR_SUPERPIXEL and [enableComputation] is true, this
84-
* will control the size of each superpixel region in pixels for the generated referenced image
85-
* @param superpixelRuler if type is COLOR_SUPERPIXEL and [enableComputation] is true, this will
86-
* control the superpixel smoothness factor for the generated referenced image
83+
* @param superpixelRegionSize if type is [ImagenControlType.COLOR_SUPERPIXEL] and
84+
* [enableComputation] is true, this will control the size of each superpixel region in pixels for
85+
* the generated referenced image
86+
* @param superpixelRuler if type is [ImagenControlType.COLOR_SUPERPIXEL] and [enableComputation] is
87+
* true, this will control the superpixel smoothness factor for the generated referenced image
8788
*/
8889
@PublicPreviewAPI
8990
public class ImagenControlReference(
@@ -102,9 +103,8 @@ public class ImagenControlReference(
102103
) {}
103104

104105
/**
105-
* Represents a reference image for Imagen editing which will mask of a region to be edited. This
106-
* image (generated or provided) should contain only black and white pixels, with black representing
107-
* parts of the image which should not change.
106+
* Represents a mask for Imagen editing. This image (generated or provided) should contain only
107+
* black and white pixels, with black representing parts of the image which should not change.
108108
*/
109109
@PublicPreviewAPI
110110
public abstract class ImagenMaskReference
@@ -113,17 +113,16 @@ internal constructor(maskConfig: ImagenMaskConfig, image: ImagenInlineImage? = n
113113

114114
public companion object {
115115
/**
116-
* Generates these two reference images in order:
116+
* Generates two reference images of [ImagenRawImage] and [ImagenRawMask]. These images are
117+
* generated in this order:
117118
* * One [ImagenRawImage] containing the original image, padded out to the new dimensions with
118119
* black pixels, with the original image placed at the given placement
119120
* * One [ImagenRawMask] of the same dimensions containing white everywhere except at the
120-
* placement original image.
121-
*
122-
* This is the format expected by Imagen for outpainting requests.
121+
* placement original image. This is the format expected by Imagen for outpainting requests.
123122
*
124123
* @param image the original image
125-
* @param newDimensions the new dimensions for outpainting. This *must* be more than the
126-
* original image.
124+
* @param newDimensions the new dimensions for outpainting. These new dimensions *must* be more
125+
* than the original image.
127126
* @param newPosition the placement of the original image within the new outpainted image.
128127
*/
129128
@JvmOverloads
@@ -132,6 +131,29 @@ internal constructor(maskConfig: ImagenMaskConfig, image: ImagenInlineImage? = n
132131
image: ImagenInlineImage,
133132
newDimensions: Dimensions,
134133
newPosition: ImagenImagePlacement = ImagenImagePlacement.CENTER,
134+
): List<ImagenReferenceImage> =
135+
generateMaskAndPadForOutpainting(image, newDimensions, newPosition, 0.01)
136+
137+
/**
138+
* Generates two reference images of [ImagenRawImage] and [ImagenRawMask]. These images are
139+
* generated in this order:
140+
* * One [ImagenRawImage] containing the original image, padded out to the new dimensions with
141+
* black pixels, with the original image placed at the given placement
142+
* * One [ImagenRawMask] of the same dimensions containing white everywhere except at the
143+
* placement original image. This is the format expected by Imagen for outpainting requests.
144+
*
145+
* @param image the original image
146+
* @param newDimensions the new dimensions for outpainting. These new dimensions *must* be more
147+
* than the original image.
148+
* @param newPosition the placement of the original image within the new outpainted image.
149+
* @param dilation the dilation for the outpainting mask. See: [ImagenRawMask].
150+
*/
151+
@JvmStatic
152+
public fun generateMaskAndPadForOutpainting(
153+
image: ImagenInlineImage,
154+
newDimensions: Dimensions,
155+
newPosition: ImagenImagePlacement = ImagenImagePlacement.CENTER,
156+
dilation: Double = 0.01
135157
): List<ImagenReferenceImage> {
136158
val originalBitmap = image.asBitmap()
137159
if (
@@ -181,7 +203,7 @@ internal constructor(maskConfig: ImagenMaskConfig, image: ImagenInlineImage? = n
181203
newImageCanvas.drawBitmap(originalBitmap, null, normalizedImageRectangle, null)
182204
return listOf(
183205
ImagenRawImage(newImageBitmap.toImagenInlineImage()),
184-
ImagenRawMask(maskBitmap.toImagenInlineImage()),
206+
ImagenRawMask(maskBitmap.toImagenInlineImage(), dilation),
185207
)
186208
}
187209
}
@@ -190,8 +212,8 @@ internal constructor(maskConfig: ImagenMaskConfig, image: ImagenInlineImage? = n
190212
/**
191213
* A generated mask image which will auto-detect and mask out the background. The background will be
192214
* white, and the foreground black
193-
* @param dilation the amount to dilate the mask, this can help smooth the borders of an edit and
194-
* make it seem more convincing. For example, 0.05 would dilate the mask 5%.
215+
* @param dilation the amount to dilate the mask. This can help smooth the borders of an edit and
216+
* make it seem more convincing. For example, `0.05` will dilate the mask 5%.
195217
*/
196218
@PublicPreviewAPI
197219
public class ImagenBackgroundMask(dilation: Double? = null) :
@@ -200,21 +222,20 @@ public class ImagenBackgroundMask(dilation: Double? = null) :
200222
/**
201223
* A generated mask image which will auto-detect and mask out the foreground. The background will be
202224
* black, and the foreground white
203-
* @param dilation the amount to dilate the mask, this can help smooth the borders of an edit and
204-
* make it seem more convincing. For example, 0.05 would dilate the mask 5%.
225+
* @param dilation the amount to dilate the mask. This can help smooth the borders of an edit and
226+
* make it seem more convincing. For example, `0.05` will dilate the mask 5%.
205227
*/
206228
@PublicPreviewAPI
207229
public class ImagenForegroundMask(dilation: Double? = null) :
208230
ImagenMaskReference(maskConfig = ImagenMaskConfig(ImagenMaskMode.FOREGROUND, dilation)) {}
209231

210232
/**
211-
* Represents a reference image for Imagen editing which will mask of a region to be edited. This
212-
* image should contain only black and white pixels, with black representing parts of the image
213-
* which should not change.
233+
* Represents a mask for Imagen editing. This image should contain only black and white pixels, with
234+
* black representing parts of the image which should not change.
214235
*
215236
* @param mask the mask image
216-
* @param dilation the amount to dilate the mask, this can help smooth the borders of an edit and
217-
* make it seem more convincing. For example, 0.05 would dilate the mask 5%.
237+
* @param dilation the amount to dilate the mask. This can help smooth the borders of an edit and
238+
* make it seem more convincing. For example, `0.05` will dilate the mask 5%.
218239
*/
219240
@PublicPreviewAPI
220241
public class ImagenRawMask(mask: ImagenInlineImage, dilation: Double? = null) :
@@ -226,11 +247,11 @@ public class ImagenRawMask(mask: ImagenInlineImage, dilation: Double? = null) :
226247
/**
227248
* Represents a generated mask for Imagen editing which masks out certain objects using object
228249
* detection.
229-
* @param classes the list of segmentation IDs for objects to detect and mask out. See
230-
* [here](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/imagen-api-edit#segment-ids)
231-
* for a list of segmentation IDs
232-
* @param dilation the amount to dilate the mask, this can help smooth the borders of an edit and
233-
* make it seem more convincing. For example, 0.05 would dilate the mask 5%.
250+
* @param classes the list of segmentation IDs for objects to detect and mask out. Find a
251+
* [list of segmentation IDs](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/imagen-api-edit#segment-ids)
252+
* in the Vertex AI documentation.
253+
* @param dilation the amount to dilate the mask. This can help smooth the borders of an edit and
254+
* make it seem more convincing. For example, `0.05` will dilate the mask 5%.
234255
*/
235256
@PublicPreviewAPI
236257
public class ImagenSemanticMask(classes: List<Int>, dilation: Double? = null) :

firebase-ai/src/test/java/com/google/firebase/ai/DevAPIUnarySnapshotTests.kt

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,12 @@ import com.google.firebase.ai.type.ResponseStoppedException
2222
import com.google.firebase.ai.type.ServerException
2323
import com.google.firebase.ai.util.goldenDevAPIUnaryFile
2424
import io.kotest.assertions.throwables.shouldThrow
25+
import io.kotest.matchers.collections.shouldBeEmpty
2526
import io.kotest.matchers.collections.shouldNotBeEmpty
2627
import io.kotest.matchers.nulls.shouldBeNull
2728
import io.kotest.matchers.nulls.shouldNotBeNull
2829
import io.kotest.matchers.should
2930
import io.kotest.matchers.shouldBe
30-
import io.kotest.matchers.shouldNotBe
3131
import io.ktor.http.HttpStatusCode
3232
import kotlin.time.Duration.Companion.seconds
3333
import kotlinx.coroutines.withTimeout
@@ -42,9 +42,24 @@ internal class DevAPIUnarySnapshotTests {
4242
withTimeout(testTimeout) {
4343
val response = model.generateContent("prompt")
4444

45-
response.candidates.isEmpty() shouldBe false
45+
response.candidates.shouldNotBeEmpty()
4646
response.candidates.first().finishReason shouldBe FinishReason.STOP
47-
response.candidates.first().content.parts.isEmpty() shouldBe false
47+
response.candidates.first().content.parts.shouldNotBeEmpty()
48+
}
49+
}
50+
51+
@Test
52+
fun `only prompt feedback reply`() =
53+
goldenDevAPIUnaryFile("unary-failure-only-prompt-feedback.json") {
54+
withTimeout(testTimeout) {
55+
val response = model.generateContent("prompt")
56+
57+
response.candidates.shouldBeEmpty()
58+
59+
// Check response from accessors
60+
response.text.shouldBeNull()
61+
response.functionCalls.shouldBeEmpty()
62+
response.inlineDataParts.shouldBeEmpty()
4863
}
4964
}
5065

@@ -54,9 +69,9 @@ internal class DevAPIUnarySnapshotTests {
5469
withTimeout(testTimeout) {
5570
val response = model.generateContent("prompt")
5671

57-
response.candidates.isEmpty() shouldBe false
72+
response.candidates.shouldNotBeEmpty()
5873
response.candidates.first().finishReason shouldBe FinishReason.STOP
59-
response.candidates.first().content.parts.isEmpty() shouldBe false
74+
response.candidates.first().content.parts.shouldNotBeEmpty()
6075
}
6176
}
6277

@@ -66,11 +81,11 @@ internal class DevAPIUnarySnapshotTests {
6681
withTimeout(testTimeout) {
6782
val response = model.generateContent("prompt")
6883

69-
response.candidates.isEmpty() shouldBe false
84+
response.candidates.shouldNotBeEmpty()
7085
response.candidates.first().citationMetadata?.citations?.size shouldBe 4
7186
response.candidates.first().citationMetadata?.citations?.forEach {
72-
it.startIndex shouldNotBe null
73-
it.endIndex shouldNotBe null
87+
it.startIndex.shouldNotBeNull()
88+
it.endIndex.shouldNotBeNull()
7489
}
7590
}
7691
}

0 commit comments

Comments
 (0)