@@ -27,7 +27,7 @@ namespace Firebase.Sample.FirebaseAI {
27
27
using System . Threading . Tasks ;
28
28
using Google . MiniJSON ;
29
29
using UnityEngine ;
30
- using UnityEngine . Video ;
30
+ using UnityEngine . Networking ;
31
31
using System . IO ;
32
32
#if INCLUDE_FIREBASE_AUTH
33
33
using Firebase . Auth ;
@@ -70,6 +70,8 @@ protected override void Start() {
70
70
TestCountTokens ,
71
71
TestYoutubeLink ,
72
72
TestGenerateImage ,
73
+ TestImagenGenerateImage ,
74
+ TestImagenGenerateImageOptions
73
75
} ;
74
76
// Set of tests that only run the single time.
75
77
Func < Task > [ ] singleTests = {
@@ -86,6 +88,9 @@ protected override void Start() {
86
88
InternalTestBasicResponseLongUsageMetadata ,
87
89
InternalTestGoogleAIBasicReplyShort ,
88
90
InternalTestGoogleAICitations ,
91
+ InternalTestGenerateImagesBase64 ,
92
+ InternalTestGenerateImagesAllFiltered ,
93
+ InternalTestGenerateImagesBase64SomeFiltered ,
89
94
} ;
90
95
91
96
// Create the set of tests, combining the above lists.
@@ -443,7 +448,7 @@ async Task TestEnumSchemaResponse(Backend backend) {
443
448
generationConfig : new GenerationConfig (
444
449
responseMimeType : "text/x.enum" ,
445
450
responseSchema : Schema . Enum ( new string [ ] { enumValue } ) ) ) ;
446
-
451
+
447
452
var response = await model . GenerateContentAsync (
448
453
"Hello, I am testing setting the response schema to an enum." ) ;
449
454
@@ -459,7 +464,7 @@ async Task TestAnyOfSchemaResponse(Backend backend) {
459
464
Schema . AnyOf ( new [ ] { Schema . Int ( ) , Schema . String ( ) } ) ,
460
465
minItems : 2 ,
461
466
maxItems : 6 ) ) ) ;
462
-
467
+
463
468
var response = await model . GenerateContentAsync (
464
469
"Hello, I am testing setting the response schema with an array, cause you give me some random values." ) ;
465
470
@@ -658,7 +663,7 @@ async Task TestYoutubeLink(Backend backend) {
658
663
async Task TestGenerateImage ( Backend backend ) {
659
664
var model = GetFirebaseAI ( backend ) . GetGenerativeModel ( "gemini-2.0-flash-exp" ,
660
665
generationConfig : new GenerationConfig (
661
- responseModalities : new [ ] { ResponseModality . Text , ResponseModality . Image } )
666
+ responseModalities : new [ ] { ResponseModality . Text , ResponseModality . Image } )
662
667
) ;
663
668
664
669
GenerateContentResponse response = await model . GenerateContentAsync (
@@ -683,6 +688,58 @@ async Task TestGenerateImage(Backend backend) {
683
688
Assert ( $ "Missing expected modalities. Text: { foundText } , Image: { foundImage } ", foundText && foundImage ) ;
684
689
}
685
690
691
+ async Task TestImagenGenerateImage ( Backend backend ) {
692
+ var model = GetFirebaseAI ( backend ) . GetImagenModel ( "imagen-3.0-generate-002" ) ;
693
+
694
+ var response = await model . GenerateImagesAsync (
695
+ "Generate an image of a cartoon dog." ) ;
696
+
697
+ // We can't easily test if the image is correct, but can check other random data.
698
+ AssertEq ( "FilteredReason" , response . FilteredReason , null ) ;
699
+ AssertEq ( "Image Count" , response . Images . Count , 1 ) ;
700
+
701
+ AssertEq ( $ "Image MimeType", response . Images [ 0 ] . MimeType , "image/png" ) ;
702
+
703
+ var texture = response . Images [ 0 ] . AsTexture2D ( ) ;
704
+ Assert ( $ "Image as Texture2D", texture != null ) ;
705
+ // By default the image should be Square 1x1, so check for that.
706
+ Assert ( $ "Image Height > 0", texture . height > 0 ) ;
707
+ AssertEq ( $ "Image Height = Width", texture . height , texture . width ) ;
708
+ }
709
+
710
+ async Task TestImagenGenerateImageOptions ( Backend backend ) {
711
+ var model = GetFirebaseAI ( backend ) . GetImagenModel (
712
+ modelName : "imagen-3.0-generate-002" ,
713
+ generationConfig : new ImagenGenerationConfig (
714
+ // negativePrompt and addWatermark are not supported on this version of the model.
715
+ numberOfImages : 2 ,
716
+ aspectRatio : ImagenAspectRatio . Landscape4x3 ,
717
+ imageFormat : ImagenImageFormat . Jpeg ( 50 )
718
+ ) ,
719
+ safetySettings : new ImagenSafetySettings (
720
+ safetyFilterLevel : ImagenSafetySettings . SafetyFilterLevel . BlockLowAndAbove ,
721
+ personFilterLevel : ImagenSafetySettings . PersonFilterLevel . BlockAll ) ,
722
+ requestOptions : new RequestOptions ( timeout : TimeSpan . FromMinutes ( 1 ) ) ) ;
723
+
724
+ var response = await model . GenerateImagesAsync (
725
+ "Generate an image of a cartoon dog." ) ;
726
+
727
+ // We can't easily test if the image is correct, but can check other random data.
728
+ AssertEq ( "FilteredReason" , response . FilteredReason , null ) ;
729
+ AssertEq ( "Image Count" , response . Images . Count , 2 ) ;
730
+
731
+ for ( int i = 0 ; i < 2 ; i ++ ) {
732
+ AssertEq ( $ "Image { i } MimeType", response . Images [ i ] . MimeType , "image/jpeg" ) ;
733
+
734
+ var texture = response . Images [ i ] . AsTexture2D ( ) ;
735
+ Assert ( $ "Image { i } as Texture2D", texture != null ) ;
736
+ // By default the image should be Landscape 4x3, so check for that.
737
+ Assert ( $ "Image { i } Height > 0", texture . height > 0 ) ;
738
+ Assert ( $ "Image { i } Height < Width { texture . height } < { texture . width } ",
739
+ texture . height < texture . width ) ;
740
+ }
741
+ }
742
+
686
743
// Test providing a file from a GCS bucket (Firebase Storage) to the model.
687
744
async Task TestReadFile ( ) {
688
745
// GCS is currently only supported with VertexAI.
@@ -737,17 +794,43 @@ async Task TestReadSecureFile() {
737
794
738
795
// The url prefix to use when fetching test data to use from the separate GitHub repo.
739
796
readonly string testDataUrl =
740
- "https://raw.githubusercontent.com/FirebaseExtended/vertexai-sdk-test-data/3737ae1fe9c5ecbd55abdeabc273ef4f392cbf19 /mock-responses/" ;
797
+ "https://raw.githubusercontent.com/FirebaseExtended/vertexai-sdk-test-data/47becf9101d11ea3c568bf60b12f1c8ed9fb684e /mock-responses/" ;
741
798
readonly HttpClient httpClient = new ( ) ;
742
799
800
+ private Task < string > LoadStreamingAsset ( string fullPath ) {
801
+ TaskCompletionSource < string > tcs = new TaskCompletionSource < string > ( ) ;
802
+ UnityWebRequest request = UnityWebRequest . Get ( fullPath ) ;
803
+ request . SendWebRequest ( ) . completed += ( _ ) => {
804
+ if ( request . result == UnityWebRequest . Result . Success ) {
805
+ tcs . SetResult ( request . downloadHandler . text ) ;
806
+ } else {
807
+ tcs . SetResult ( null ) ;
808
+ }
809
+ } ;
810
+ return tcs . Task ;
811
+ }
812
+
743
813
// Gets the Json test data from the given filename, potentially downloading from a GitHub repo.
744
814
private async Task < Dictionary < string , object > > GetJsonTestData ( string filename ) {
745
- // TODO: Check if the file is available locally first
815
+ string jsonString = null ;
816
+ // First, try to load the file from StreamingAssets
817
+ string localPath = Path . Combine ( Application . streamingAssetsPath , "TestData" , filename ) ;
818
+ if ( localPath . StartsWith ( "jar" ) || localPath . StartsWith ( "http" ) ) {
819
+ // Special case to access StreamingAsset content on Android
820
+ jsonString = await LoadStreamingAsset ( localPath ) ;
821
+ } else if ( File . Exists ( localPath ) ) {
822
+ jsonString = File . ReadAllText ( localPath ) ;
823
+ }
746
824
747
- var response = await httpClient . SendAsync ( new HttpRequestMessage ( HttpMethod . Get , testDataUrl + filename ) ) ;
748
- response . EnsureSuccessStatusCode ( ) ;
749
-
750
- string jsonString = await response . Content . ReadAsStringAsync ( ) ;
825
+ if ( string . IsNullOrEmpty ( jsonString ) ) {
826
+ DebugLog ( "Failed to load from local file, downloading..." ) ;
827
+ var response = await httpClient . SendAsync ( new HttpRequestMessage ( HttpMethod . Get , testDataUrl + filename ) ) ;
828
+ response . EnsureSuccessStatusCode ( ) ;
829
+
830
+ jsonString = await response . Content . ReadAsStringAsync ( ) ;
831
+ } else {
832
+ DebugLog ( "Using local file" ) ;
833
+ }
751
834
752
835
return Json . Deserialize ( jsonString ) as Dictionary < string , object > ;
753
836
}
@@ -862,7 +945,7 @@ async Task InternalTestBasicReplyShort() {
862
945
async Task InternalTestCitations ( ) {
863
946
Dictionary < string , object > json = await GetVertexJsonTestData ( "unary-success-citations.json" ) ;
864
947
GenerateContentResponse response = GenerateContentResponse . FromJson ( json , FirebaseAI . Backend . InternalProvider . VertexAI ) ;
865
-
948
+
866
949
ValidateTextPart ( response , "Some information cited from an external source" ) ;
867
950
868
951
CitationMetadata ? metadata = response . Candidates . First ( ) . CitationMetadata ;
@@ -1050,7 +1133,7 @@ async Task InternalTestBasicResponseLongUsageMetadata() {
1050
1133
// Test that parsing a basic short reply from Google AI endpoint works as expected.
1051
1134
// https://github.com/FirebaseExtended/vertexai-sdk-test-data/blob/main/mock-responses/googleai/unary-success-basic-reply-short.txt
1052
1135
async Task InternalTestGoogleAIBasicReplyShort ( ) {
1053
- Dictionary < string , object > json = await GetGoogleAIJsonTestData ( "unary-success-basic-reply-short.txt " ) ; //
1136
+ Dictionary < string , object > json = await GetGoogleAIJsonTestData ( "unary-success-basic-reply-short.json " ) ; //
1054
1137
GenerateContentResponse response = GenerateContentResponse . FromJson ( json , FirebaseAI . Backend . InternalProvider . GoogleAI ) ;
1055
1138
1056
1139
ValidateTextPart ( response , "Google's headquarters, also known as the Googleplex, is located in **Mountain View, California**.\n " ) ;
@@ -1076,12 +1159,12 @@ async Task InternalTestGoogleAIBasicReplyShort() {
1076
1159
// Test parsing a Google AI format response with citations.
1077
1160
// Based on: https://github.com/FirebaseExtended/vertexai-sdk-test-data/blob/main/mock-responses/googleai/unary-success-citations.txt
1078
1161
async Task InternalTestGoogleAICitations ( ) {
1079
- Dictionary < string , object > json = await GetGoogleAIJsonTestData ( "unary-success-citations.txt " ) ;
1162
+ Dictionary < string , object > json = await GetGoogleAIJsonTestData ( "unary-success-citations.json " ) ;
1080
1163
GenerateContentResponse response = GenerateContentResponse . FromJson ( json , FirebaseAI . Backend . InternalProvider . GoogleAI ) ;
1081
1164
1082
1165
// Validate Text Part (check start and end)
1083
1166
string expectedStart = "Okay, let's break down quantum mechanics." ;
1084
- string expectedEnd = "foundation for many technologies, including: \n " ;
1167
+ string expectedEnd = "area of physics! " ;
1085
1168
Assert ( "Candidate count" , response . Candidates . Count ( ) == 1 ) ;
1086
1169
Candidate candidate = response . Candidates . First ( ) ;
1087
1170
AssertEq ( "Content role" , candidate . Content . Role , "model" ) ;
@@ -1153,5 +1236,54 @@ async Task InternalTestGoogleAICitations() {
1153
1236
AssertEq ( "CandidatesTokensDetails[0].Modality" , candidatesDetails [ 0 ] . Modality , ContentModality . Text ) ;
1154
1237
AssertEq ( "CandidatesTokensDetails[0].TokenCount" , candidatesDetails [ 0 ] . TokenCount , 1667 ) ;
1155
1238
}
1239
+
1240
+ async Task InternalTestGenerateImagesBase64 ( ) {
1241
+ Dictionary < string , object > json = await GetVertexJsonTestData ( "unary-success-generate-images-base64.json" ) ;
1242
+ var response = ImagenGenerationResponse < ImagenInlineImage > . FromJson ( json ) ;
1243
+
1244
+ AssertEq ( "FilteredReason" , response . FilteredReason , null ) ;
1245
+ AssertEq ( "Image Count" , response . Images . Count , 4 ) ;
1246
+
1247
+ for ( int i = 0 ; i < response . Images . Count ; i ++ ) {
1248
+ var image = response . Images [ i ] ;
1249
+ AssertEq ( $ "Image { i } MimeType", image . MimeType , "image/png" ) ;
1250
+ Assert ( $ "Image { i } Length: { image . Data . Length } ", image . Data . Length > 0 ) ;
1251
+
1252
+ var texture = image . AsTexture2D ( ) ;
1253
+ Assert ( $ "Failed to convert Image { i } ", texture != null ) ;
1254
+ }
1255
+ }
1256
+
1257
+ async Task InternalTestGenerateImagesAllFiltered ( ) {
1258
+ Dictionary < string , object > json = await GetVertexJsonTestData ( "unary-failure-generate-images-all-filtered.json" ) ;
1259
+ var response = ImagenGenerationResponse < ImagenInlineImage > . FromJson ( json ) ;
1260
+
1261
+ AssertEq ( "FilteredReason" , response . FilteredReason ,
1262
+ "Unable to show generated images. All images were filtered out because " +
1263
+ "they violated Vertex AI's usage guidelines. You will not be charged for " +
1264
+ "blocked images. Try rephrasing the prompt. If you think this was an error, " +
1265
+ "send feedback. Support codes: 39322892, 29310472" ) ;
1266
+ AssertEq ( "Image Count" , response . Images . Count , 0 ) ;
1267
+ }
1268
+
1269
+ async Task InternalTestGenerateImagesBase64SomeFiltered ( ) {
1270
+ Dictionary < string , object > json = await GetVertexJsonTestData ( "unary-failure-generate-images-base64-some-filtered.json" ) ;
1271
+ var response = ImagenGenerationResponse < ImagenInlineImage > . FromJson ( json ) ;
1272
+
1273
+ AssertEq ( "FilteredReason" , response . FilteredReason ,
1274
+ "Your current safety filter threshold filtered out 2 generated images. " +
1275
+ "You will not be charged for blocked images. Try rephrasing the prompt. " +
1276
+ "If you think this was an error, send feedback." ) ;
1277
+ AssertEq ( "Image Count" , response . Images . Count , 2 ) ;
1278
+
1279
+ for ( int i = 0 ; i < response . Images . Count ; i ++ ) {
1280
+ var image = response . Images [ i ] ;
1281
+ AssertEq ( $ "Image { i } MimeType", image . MimeType , "image/png" ) ;
1282
+ Assert ( $ "Image { i } Length: { image . Data . Length } ", image . Data . Length > 0 ) ;
1283
+
1284
+ var texture = image . AsTexture2D ( ) ;
1285
+ Assert ( $ "Failed to convert Image { i } ", texture != null ) ;
1286
+ }
1287
+ }
1156
1288
}
1157
1289
}
0 commit comments