diff --git a/docs/firebaseai/FirebaseAIReadme.md b/docs/firebaseai/FirebaseAIReadme.md index f0af2675..f10a4a4f 100644 --- a/docs/firebaseai/FirebaseAIReadme.md +++ b/docs/firebaseai/FirebaseAIReadme.md @@ -3,9 +3,9 @@ Get Started with Firebase AI Thank you for installing the Firebase AI Unity SDK. -The Firebase AI Gemini API gives you access to the latest generative AI models from Google: the Gemini models. This SDK is built specifically for use with Unity and mobile developers, offering security options against unauthorized clients as well as integrations with other Firebase services. +The Firebase AI SDK for Unity gives you access to Google's state-of-the-art generative AI models. This SDK is built specifically for use with Unity and mobile developers, offering security options against unauthorized clients as well as integrations with other Firebase services. -With this, you can add AI personalization to your app, build an AI chat experience, create AI-powered optimizations and automation, and much more! +With this, you can add AI personalization to your app, build an AI chat experience, create AI-powered optimizations and automation, generate images, and much more! ### Links @@ -19,3 +19,104 @@ With this, you can add AI personalization to your app, build an AI chat experien * [Stack overflow](https://stackoverflow.com/questions/tagged/firebase) * [Slack community](https://firebase-community.slack.com/) * [Google groups](https://groups.google.com/forum/#!forum/firebase-talk) + +## Available Models + +The Firebase AI SDK for Unity currently supports the following model families: + +### Gemini API + +The Firebase AI Gemini API gives you access to the latest generative AI models from Google: the Gemini models. These models are excellent for text generation, summarization, chat applications, and more. + +_(Refer to the [Firebase documentation](https://firebase.google.com/docs/vertex-ai/gemini-models) for more detailed examples on using the Gemini API.)_ + +### Imagen API + +The Firebase AI Imagen API allows you to generate and manipulate images using Google's advanced image generation models. You can create novel images from text prompts, edit existing images, and more. + +#### Initializing ImagenModel + +First, initialize `FirebaseAI` and then get an `ImagenModel` instance. You can optionally provide generation configuration and safety settings at this stage. + +```csharp +using Firebase; +using Firebase.AI; +using UnityEngine; // Required for Debug.Log and Texture2D + +public class ImagenExample : MonoBehaviour +{ + async void Start() + { + FirebaseApp app = FirebaseApp.DefaultInstance; // Or your specific app + + // Initialize the Vertex AI backend service (recommended for Imagen) + var ai = FirebaseAI.GetInstance(app, FirebaseAI.Backend.VertexAI()); + + // Create an `ImagenModel` instance with a model that supports your use case + // Consult Imagen documentation for the latest model names. + var model = ai.GetImagenModel( + modelName: "imagen-3.0-generate-002", // Example model name, replace with a valid one + generationConfig: new ImagenGenerationConfig(numberOfImages: 1)); // Request 1 image + + // Provide an image generation prompt + var prompt = "A photo of a futuristic car driving on Mars at sunset."; + + // To generate an image and receive it as inline data (byte array) + var response = await model.GenerateImagesAsync(prompt: prompt); + + // If fewer images were generated than were requested, + // then `filteredReason` will describe the reason they were filtered out + if (!string.IsNullOrEmpty(response.FilteredReason)) { + UnityEngine.Debug.Log($"Image generation partially filtered: {response.FilteredReason}"); + } + + if (response.Images != null && response.Images.Count > 0) + { + foreach (var image in response.Images) { + // Assuming image is ImagenInlineImage + Texture2D tex = image.AsTexture2D(); + if (tex != null) + { + UnityEngine.Debug.Log($"Image generated with MIME type: {image.MimeType}, Size: {tex.width}x{tex.height}"); + // Process the image (e.g., display it on a UI RawImage) + // Example: rawImageComponent.texture = tex; + } + } + } + else + { + UnityEngine.Debug.Log("No images were generated. Check FilteredReason or logs for more details."); + } + } +} +``` + +#### Generating Images to Google Cloud Storage (GCS) + +Imagen can also output generated images directly to a Google Cloud Storage bucket. This is useful for workflows where images don't need to be immediately processed on the client. + +```csharp +// (Inside an async method, assuming 'model' is an initialized ImagenModel) +var gcsUri = new System.Uri("gs://your-gcs-bucket-name/path/to/output_image.png"); +var gcsResponse = await model.GenerateImagesAsync(prompt: "A fantasy castle in the clouds", gcsUri: gcsUri); + +if (gcsResponse.Images != null && gcsResponse.Images.Count > 0) { + foreach (var imageRef in gcsResponse.Images) { + // imageRef will be an ImagenGcsImage instance + UnityEngine.Debug.Log($"Image generation requested to GCS. Output URI: {imageRef.GcsUri}, MIME Type: {imageRef.MimeType}"); + // Further processing might involve triggering a cloud function or another backend process + // that reads from this GCS URI. + } +} +``` + +#### Configuration Options + +When working with Imagen, you can customize the generation process using several configuration structs: + +* **`ImagenGenerationConfig`**: Controls aspects like the number of images to generate (`NumberOfImages`), the desired aspect ratio (`ImagenAspectRatio`), the output image format (`ImagenImageFormat`), and whether to add a watermark (`AddWatermark`). You can also specify a `NegativePrompt`. +* **`ImagenSafetySettings`**: Allows you to configure safety filters for generated content, such as `SafetyFilterLevel` (e.g., `BlockMediumAndAbove`) and `PersonFilterLevel` (e.g., `BlockAll`). +* **`ImagenImageFormat`**: Defines the output image format. Use static methods like `ImagenImageFormat.Png()` or `ImagenImageFormat.Jpeg(int? compressionQuality = null)`. +* **`ImagenAspectRatio`**: An enum to specify common aspect ratios like `Square1x1`, `Portrait9x16`, etc. + +These configuration types are available in the `Firebase.AI` namespace. Refer to the API documentation or inline comments in the SDK for more details on their usage. diff --git a/firebaseai/src/FirebaseAI.cs b/firebaseai/src/FirebaseAI.cs index eb5c6986..5f641ac1 100644 --- a/firebaseai/src/FirebaseAI.cs +++ b/firebaseai/src/FirebaseAI.cs @@ -191,6 +191,38 @@ public LiveGenerativeModel GetLiveModel( liveGenerationConfig, tools, systemInstruction, requestOptions); } + + /// + /// Initializes an Imagen model for image generation with the given parameters. + /// + /// - Note: Refer to Imagen documentation for appropriate model names and capabilities. + /// + /// The name of the Imagen model to use. + /// The image generation parameters your model should use. + /// Safety settings for content filtering. + /// Configuration parameters for sending requests to the backend. + /// The initialized `ImagenModel` instance. + public ImagenModel GetImagenModel( + string modelName, + ImagenGenerationConfig? generationConfig = null, + ImagenSafetySettings? safetySettings = null, + RequestOptions? requestOptions = null + ) { + // Potentially add validation for modelName or other parameters if needed. + // Ensure backend compatibility if Imagen is only available on certain backends. + // For example, if Imagen is VertexAI only: + // if (_backend.Provider != Backend.InternalProvider.VertexAI) { + // throw new NotSupportedException("ImagenModel is currently only supported with the VertexAI backend."); + // } + return new ImagenModel( + _firebaseApp, + _backend, + modelName, + generationConfig, + safetySettings, + requestOptions + ); + } } } diff --git a/firebaseai/src/IImagenImage.cs b/firebaseai/src/IImagenImage.cs new file mode 100644 index 00000000..c6b948f5 --- /dev/null +++ b/firebaseai/src/IImagenImage.cs @@ -0,0 +1,5 @@ +namespace Firebase.AI { + public interface IImagenImage { + public string MimeType { get; } + } +} diff --git a/firebaseai/src/ImagenGcsImage.cs b/firebaseai/src/ImagenGcsImage.cs new file mode 100644 index 00000000..05fd3996 --- /dev/null +++ b/firebaseai/src/ImagenGcsImage.cs @@ -0,0 +1,13 @@ +using System; + +namespace Firebase.AI { + public readonly struct ImagenGcsImage : IImagenImage { + public string MimeType { get; } + public System.Uri GcsUri { get; } + + public ImagenGcsImage(string mimeType, System.Uri gcsUri) { + MimeType = mimeType; + GcsUri = gcsUri; + } + } +} diff --git a/firebaseai/src/ImagenGenerationConfig.cs b/firebaseai/src/ImagenGenerationConfig.cs new file mode 100644 index 00000000..09b9010f --- /dev/null +++ b/firebaseai/src/ImagenGenerationConfig.cs @@ -0,0 +1,52 @@ +namespace Firebase.AI { + public enum ImagenAspectRatio { + Square1x1, + Portrait9x16, + Landscape16x9, + Portrait3x4, + Landscape4x3 + } + + public readonly struct ImagenGenerationConfig { + public string NegativePrompt { get; } + public int? NumberOfImages { get; } + public ImagenAspectRatio? AspectRatio { get; } + public ImagenImageFormat? ImageFormat { get; } + public bool? AddWatermark { get; } + + public ImagenGenerationConfig( + string negativePrompt = null, + int? numberOfImages = null, + ImagenAspectRatio? aspectRatio = null, + ImagenImageFormat? imageFormat = null, + bool? addWatermark = null + ) { + NegativePrompt = negativePrompt; + NumberOfImages = numberOfImages; + AspectRatio = aspectRatio; + ImageFormat = imageFormat; + AddWatermark = addWatermark; + } + + // Helper method to convert to JSON dictionary for requests + internal System.Collections.Generic.Dictionary ToJson() { + var jsonDict = new System.Collections.Generic.Dictionary(); + if (!string.IsNullOrEmpty(NegativePrompt)) { + jsonDict["negativePrompt"] = NegativePrompt; + } + if (NumberOfImages.HasValue) { + jsonDict["numberOfImages"] = NumberOfImages.Value; + } + if (AspectRatio.HasValue) { + jsonDict["aspectRatio"] = AspectRatio.Value.ToString(); + } + if (ImageFormat.HasValue) { + jsonDict["imageFormat"] = ImageFormat.Value.ToJson(); + } + if (AddWatermark.HasValue) { + jsonDict["addWatermark"] = AddWatermark.Value; + } + return jsonDict; + } + } +} diff --git a/firebaseai/src/ImagenGenerationResponse.cs b/firebaseai/src/ImagenGenerationResponse.cs new file mode 100644 index 00000000..88476d50 --- /dev/null +++ b/firebaseai/src/ImagenGenerationResponse.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using System.Linq; +using Google.MiniJSON; // Assuming MiniJSON is available and used elsewhere in the SDK + +namespace Firebase.AI { + public readonly struct ImagenGenerationResponse where T : IImagenImage { + public IReadOnlyList Images { get; } + public string FilteredReason { get; } + + // Internal constructor for creating from parsed data + internal ImagenGenerationResponse(IReadOnlyList images, string filteredReason) { + Images = images; + FilteredReason = filteredReason; + } + + // Static factory method to parse JSON + // Note: This is a simplified parser. Error handling and robustness should match SDK standards. + internal static ImagenGenerationResponse FromJson(string jsonString) { + if (string.IsNullOrEmpty(jsonString)) { + return new ImagenGenerationResponse(System.Array.Empty(), "Empty or null JSON response"); + } + + object jsonData = Json.Deserialize(jsonString); + if (!(jsonData is Dictionary responseMap)) { + return new ImagenGenerationResponse(System.Array.Empty(), "Invalid JSON format: Expected a dictionary at the root."); + } + + List images = new List(); + string filteredReason = responseMap.ContainsKey("filteredReason") ? responseMap["filteredReason"] as string : null; + + if (responseMap.ContainsKey("images") && responseMap["images"] is List imagesList) { + foreach (var imgObj in imagesList) { + if (imgObj is Dictionary imgMap) { + string mimeType = imgMap.ContainsKey("mimeType") ? imgMap["mimeType"] as string : "application/octet-stream"; + + if (typeof(T) == typeof(ImagenInlineImage)) { + if (imgMap.ContainsKey("imageBytes") && imgMap["imageBytes"] is string base64Data) { + byte[] data = System.Convert.FromBase64String(base64Data); + images.Add((T)(IImagenImage)new ImagenInlineImage(mimeType, data)); + } + } else if (typeof(T) == typeof(ImagenGcsImage)) { + if (imgMap.ContainsKey("gcsUri") && imgMap["gcsUri"] is string uriString) { + if (System.Uri.TryCreate(uriString, System.UriKind.Absolute, out System.Uri gcsUri)) { + images.Add((T)(IImagenImage)new ImagenGcsImage(mimeType, gcsUri)); + } + } + } + } + } + } + + // If no specific images are found, but there's a top-level "image" field (for single image responses) + // This part might need adjustment based on actual API response for single vs multiple images. + // The provided API doc implies a list `Images` always. + // For now, sticking to the `images` list. + + return new ImagenGenerationResponse(images.AsReadOnly(), filteredReason); + } + } +} diff --git a/firebaseai/src/ImagenImageFormat.cs b/firebaseai/src/ImagenImageFormat.cs new file mode 100644 index 00000000..e37f3fb5 --- /dev/null +++ b/firebaseai/src/ImagenImageFormat.cs @@ -0,0 +1,34 @@ +namespace Firebase.AI { + public readonly struct ImagenImageFormat { + public enum FormatType { Png, Jpeg } + + public FormatType Type { get; } + public int? CompressionQuality { get; } // Nullable for PNG + + private ImagenImageFormat(FormatType type, int? compressionQuality = null) { + Type = type; + CompressionQuality = compressionQuality; + } + + public static ImagenImageFormat Png() { + return new ImagenImageFormat(FormatType.Png); + } + + public static ImagenImageFormat Jpeg(int? compressionQuality = null) { + if (compressionQuality.HasValue && (compressionQuality < 0 || compressionQuality > 100)) { + throw new System.ArgumentOutOfRangeException(nameof(compressionQuality), "Compression quality must be between 0 and 100."); + } + return new ImagenImageFormat(FormatType.Jpeg, compressionQuality); + } + + // Helper method to convert to JSON dictionary for requests + internal System.Collections.Generic.Dictionary ToJson() { + var jsonDict = new System.Collections.Generic.Dictionary(); + jsonDict["type"] = Type.ToString().ToLowerInvariant(); + if (Type == FormatType.Jpeg && CompressionQuality.HasValue) { + jsonDict["compressionQuality"] = CompressionQuality.Value; + } + return jsonDict; + } + } +} diff --git a/firebaseai/src/ImagenInlineImage.cs b/firebaseai/src/ImagenInlineImage.cs new file mode 100644 index 00000000..24f7ac67 --- /dev/null +++ b/firebaseai/src/ImagenInlineImage.cs @@ -0,0 +1,27 @@ +using UnityEngine; + +namespace Firebase.AI { + public readonly struct ImagenInlineImage : IImagenImage { + public string MimeType { get; } + public byte[] Data { get; } + + public ImagenInlineImage(string mimeType, byte[] data) { + MimeType = mimeType; + Data = data; + } + + public UnityEngine.Texture2D AsTexture2D() { + // Implementation will be added in a later step. + // For now, it can return null or throw a NotImplementedException. + if (Data == null || Data.Length == 0) { + return null; + } + Texture2D tex = new Texture2D(2, 2); // Dimensions will be determined by image data + // ImageConversion.LoadImage will resize the texture dimensions. + if (ImageConversion.LoadImage(tex, Data)) { + return tex; + } + return null; + } + } +} diff --git a/firebaseai/src/ImagenModel.cs b/firebaseai/src/ImagenModel.cs new file mode 100644 index 00000000..82bbb0a2 --- /dev/null +++ b/firebaseai/src/ImagenModel.cs @@ -0,0 +1,163 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Google.MiniJSON; // Assuming MiniJSON +using Firebase.AI.Internal; // For FirebaseInterops and potentially other internal helpers + +namespace Firebase.AI { + public class ImagenModel { + private readonly FirebaseApp _firebaseApp; + private readonly FirebaseAI.Backend _backend; + private readonly string _modelName; + private readonly ImagenGenerationConfig? _generationConfig; + private readonly ImagenSafetySettings? _safetySettings; + private readonly RequestOptions? _requestOptions; + private readonly HttpClient _httpClient; + + internal ImagenModel( + FirebaseApp firebaseApp, + FirebaseAI.Backend backend, + string modelName, + ImagenGenerationConfig? generationConfig, + ImagenSafetySettings? safetySettings, + RequestOptions? requestOptions) { + _firebaseApp = firebaseApp ?? throw new ArgumentNullException(nameof(firebaseApp)); + _backend = backend; // Assuming Backend is a struct and already validated + _modelName = !string.IsNullOrWhiteSpace(modelName) ? modelName + : throw new ArgumentException("Model name cannot be null or whitespace.", nameof(modelName)); + _generationConfig = generationConfig; + _safetySettings = safetySettings; + _requestOptions = requestOptions; + + _httpClient = new HttpClient { + Timeout = _requestOptions?.Timeout ?? RequestOptions.DefaultTimeout + }; + } + + public Task> GenerateImagesAsync( + string prompt, CancellationToken cancellationToken = default) { + return GenerateImagesAsyncInternal(prompt, null, cancellationToken); + } + + public Task> GenerateImagesAsync( + string prompt, System.Uri gcsUri, CancellationToken cancellationToken = default) { + if (gcsUri == null) throw new ArgumentNullException(nameof(gcsUri)); + return GenerateImagesAsyncInternal(prompt, gcsUri, cancellationToken); + } + + private async Task> GenerateImagesAsyncInternal( + string prompt, System.Uri gcsUri, CancellationToken cancellationToken) where T : IImagenImage { + if (string.IsNullOrWhiteSpace(prompt)) { + throw new ArgumentException("Prompt cannot be null or whitespace.", nameof(prompt)); + } + + HttpRequestMessage request = new(HttpMethod.Post, GetGenerateImagesURL()); + await SetRequestHeaders(request); + + string bodyJson = MakeGenerateImagesRequest(prompt, gcsUri); + request.Content = new StringContent(bodyJson, Encoding.UTF8, "application/json"); + + #if FIREBASE_LOG_REST_CALLS + UnityEngine.Debug.Log($"Imagen Request: {bodyJson}"); + #endif + + try { + var response = await _httpClient.SendAsync(request, cancellationToken); + await ValidateHttpResponse(response); // Similar to GenerativeModel's helper + + string result = await response.Content.ReadAsStringAsync(); + + #if FIREBASE_LOG_REST_CALLS + UnityEngine.Debug.Log($"Imagen Response: {result}"); + #endif + + return ImagenGenerationResponse.FromJson(result); + } catch (HttpRequestException e) { + // Log or handle more gracefully + UnityEngine.Debug.LogError($"Imagen API request failed: {e.Message}"); + throw; // Re-throw or wrap in a Firebase-specific exception + } + } + + private string GetGenerateImagesURL() { + // Construct the URL based on the backend provider + // This is an example, the exact URL structure needs to be verified against Imagen API docs + // Assuming VertexAI backend for Imagen, similar to Gemini + if (_backend.Provider == FirebaseAI.Backend.InternalProvider.VertexAI) { + // Example: "https://firebaseml.googleapis.com/v1beta/projects/{PROJECT_ID}/locations/{LOCATION}/publishers/google/models/{MODEL_NAME}:generateImages" + // Note: The problem description uses "firebasevertexai.googleapis.com" for Gemini. Assuming similar for Imagen. + return $"https://firebasevertexai.googleapis.com/v1beta/projects/{_firebaseApp.Options.ProjectId}/locations/{_backend.Location}/publishers/google/models/{_modelName}:generateImages"; + } + // Fallback or error for other backends if Imagen is Vertex-specific + throw new NotSupportedException($"Backend {_backend.Provider} is not supported for ImagenModel."); + } + + private async Task SetRequestHeaders(HttpRequestMessage request) { + // Similar to GenerativeModel.SetRequestHeaders + request.Headers.Add("x-goog-api-key", _firebaseApp.Options.ApiKey); + string version = FirebaseInterops.GetVersionInfoSdkVersion(); // Assuming this exists + request.Headers.Add("x-goog-api-client", $"gl-csharp/fire-{version}"); // Adjusted client name + if (FirebaseInterops.GetIsDataCollectionDefaultEnabled(_firebaseApp)) { + request.Headers.Add("X-Firebase-AppId", _firebaseApp.Options.AppId); + } + // Add additional Firebase tokens to the header. + await FirebaseInterops.AddFirebaseTokensAsync(request, _firebaseApp); + } + + private string MakeGenerateImagesRequest(string prompt, System.Uri gcsUri) { + var requestDict = new Dictionary { + ["prompt"] = new Dictionary { { "text", prompt } } // Assuming prompt is always text for now + }; + + if (_generationConfig.HasValue) { + var configDict = _generationConfig.Value.ToJson(); + foreach(var kvp in configDict) requestDict[kvp.Key] = kvp.Value; + } + + if (_safetySettings.HasValue) { + // Assuming safety settings are top-level in the request body + // This might need to be nested under a specific key like "safetySettings" + var safetyDict = _safetySettings.Value.ToJson(); + foreach(var kvp in safetyDict) requestDict[kvp.Key] = kvp.Value; + } + + if (gcsUri != null) { + // Add GCS URI for output, structure depends on API spec + requestDict["outputGcsUri"] = gcsUri.ToString(); + } + + // Add other parameters like "model" if required by the backend at this stage. + // requestDict["model"] = _modelName; // Or prefixed model path + + return Json.Serialize(requestDict); + } + + // Helper function to throw an exception if the Http Response indicates failure. + // Copied from GenerativeModel.cs for now, consider moving to a shared utility if common + private async Task ValidateHttpResponse(HttpResponseMessage response) { + if (response.IsSuccessStatusCode) { + return; + } + + string errorContent = "No error content available."; + if (response.Content != null) { + try { + errorContent = await response.Content.ReadAsStringAsync(); + } catch (Exception readEx) { + errorContent = $"Failed to read error content: {readEx.Message}"; + } + } + var ex = new HttpRequestException( + $"HTTP request failed with status code: {(int)response.StatusCode} ({response.ReasonPhrase}).\n" + + $"Error Content: {errorContent}" + ); + UnityEngine.Debug.LogError($"Request failed: {ex.Message} Full error: {errorContent}"); + throw ex; + } + } +} diff --git a/firebaseai/src/ImagenSafetySettings.cs b/firebaseai/src/ImagenSafetySettings.cs new file mode 100644 index 00000000..0ffcbcf8 --- /dev/null +++ b/firebaseai/src/ImagenSafetySettings.cs @@ -0,0 +1,39 @@ +namespace Firebase.AI { + public readonly struct ImagenSafetySettings { + public enum SafetyFilterLevel { + BlockLowAndAbove, + BlockMediumAndAbove, + BlockOnlyHigh, + BlockNone + } + + public enum PersonFilterLevel { + BlockAll, + AllowAdult, + AllowAll + } + + public SafetyFilterLevel? SafetyFilter { get; } + public PersonFilterLevel? PersonFilter { get; } + + public ImagenSafetySettings( + SafetyFilterLevel? safetyFilterLevel = null, + PersonFilterLevel? personFilterLevel = null + ) { + SafetyFilter = safetyFilterLevel; + PersonFilter = personFilterLevel; + } + + // Helper method to convert to JSON dictionary for requests + internal System.Collections.Generic.Dictionary ToJson() { + var jsonDict = new System.Collections.Generic.Dictionary(); + if (SafetyFilter.HasValue) { + jsonDict["safetyFilter"] = SafetyFilter.Value.ToString(); + } + if (PersonFilter.HasValue) { + jsonDict["personFilter"] = PersonFilter.Value.ToString(); + } + return jsonDict; + } + } +} diff --git a/firebaseai/testapp/Assets/Firebase/Sample/AI/ImagenTest.cs b/firebaseai/testapp/Assets/Firebase/Sample/AI/ImagenTest.cs new file mode 100644 index 00000000..16fffabb --- /dev/null +++ b/firebaseai/testapp/Assets/Firebase/Sample/AI/ImagenTest.cs @@ -0,0 +1,237 @@ +using UnityEngine; +using Firebase; +using Firebase.AI; +using System.Threading.Tasks; +using System.Collections.Generic; // For IReadOnlyList +using System.Threading; // Required for ContinueWithOnMainThread if not implicitly available through Firebase.Extensions + +// It's good practice to ensure Firebase.Extensions is included for ContinueWithOnMainThread +// If it's not automatically part of the testapp's setup, this might be needed: +// using Firebase.Extensions; + +public class ImagenTest : MonoBehaviour { + private FirebaseApp app; + private FirebaseAI ai; + // NOTE: Replace with an actual, available Imagen model for testing. + // This model name is a placeholder and might not be valid. + // Consult Imagen documentation for suitable test model names. + private const string TestImagenModelName = "gemini-1.5-flash-preview-0514"; // Placeholder, needs valid Imagen model + private const string TestGcsBucketPath = "gs://your-firebase-project-bucket/imagen_test_output/"; // Replace! + + void Start() { + Debug.Log("ImagenTest: Initializing Firebase..."); + Firebase.FirebaseApp.CheckAndFixDependenciesAsync().ContinueWithOnMainThread(task => { + if (task.Result == Firebase.DependencyStatus.Available) { + app = Firebase.FirebaseApp.DefaultInstance; + // Assuming Imagen is primarily a Vertex AI feature based on previous class structures. + // If GoogleAI backend is also supported for Imagen, this could be configurable. + ai = FirebaseAI.GetInstance(app, FirebaseAI.Backend.VertexAI("us-central1")); + Debug.Log("ImagenTest: Firebase initialized. Starting tests..."); + RunAllTests(); + } else { + Debug.LogError("ImagenTest: Could not resolve all Firebase dependencies: " + task.Result); + } + }); + } + + async void RunAllTests() { + Debug.Log("===== Starting Imagen Tests ====="); + await TestGenerateInlineImageSimple(); + await TestGenerateInlineImageWithConfig(); + await TestGenerateInlineImageWithSafetySettings(); + await TestGenerateGcsImageSimple(); + // TODO: Add a test for FilteredReason if a reliable way to trigger it can be found. + Debug.Log("===== Imagen Tests Concluded ====="); + } + + async Task TestGenerateInlineImageSimple() { + Debug.Log("TestGenerateInlineImageSimple: Starting..."); + if (ai == null) { + Debug.LogError("TestGenerateInlineImageSimple: FirebaseAI not initialized."); + return; + } + + var model = ai.GetImagenModel(TestImagenModelName); + var prompt = "A watercolor painting of a serene lake at sunset."; + + try { + var response = await model.GenerateImagesAsync(prompt: prompt); + + if (response.Images != null && response.Images.Count > 0) { + Debug.Log($"TestGenerateInlineImageSimple: Received {response.Images.Count} image(s)."); + bool allImagesValid = true; + foreach (var image in response.Images) { + if (image.Data != null && image.Data.Length > 0) { + Debug.Log($"TestGenerateInlineImageSimple: Image data is not empty (MIME: {image.MimeType})."); + Texture2D tex = image.AsTexture2D(); + if (tex != null && tex.width > 0 && tex.height > 0) { + Debug.Log($"TestGenerateInlineImageSimple: AsTexture2D() successful. Texture size: {tex.width}x{tex.height}"); + // Clean up texture if not needed further + Object.Destroy(tex); + } else { + Debug.LogError("TestGenerateInlineImageSimple: AsTexture2D() failed or returned invalid texture."); + allImagesValid = false; + } + } else { + Debug.LogError("TestGenerateInlineImageSimple: Image data is null or empty."); + allImagesValid = false; + } + } + if (allImagesValid) Debug.Log("TestGenerateInlineImageSimple: PASS (All images processed successfully)"); + else Debug.LogError("TestGenerateInlineImageSimple: FAIL (One or more images had issues, see logs)"); + + } else if (response.FilteredReason != null) { + Debug.LogWarning($"TestGenerateInlineImageSimple: No images received. FilteredReason: {response.FilteredReason}. This might be a PASS if the prompt was designed to be filtered."); + } + else { + Debug.LogError($"TestGenerateInlineImageSimple: FAIL - No images received and no FilteredReason provided."); + } + } catch (System.Exception e) { + Debug.LogError($"TestGenerateInlineImageSimple: FAILED with exception: {e}"); + } + } + + async Task TestGenerateInlineImageWithConfig() { + Debug.Log("TestGenerateInlineImageWithConfig: Starting..."); + if (ai == null) { + Debug.LogError("TestGenerateInlineImageWithConfig: FirebaseAI not initialized."); + return; + } + + // Request 2 images, specific aspect ratio + var config = new ImagenGenerationConfig( + numberOfImages: 2, + aspectRatio: ImagenAspectRatio.Square1x1, + imageFormat: ImagenImageFormat.Jpeg(80) // Request JPEG with quality + ); + var model = ai.GetImagenModel(modelName: TestImagenModelName, generationConfig: config); + var prompt = "Two robots playing poker in a futuristic casino, 1x1 aspect ratio, jpeg format"; + + try { + var response = await model.GenerateImagesAsync(prompt: prompt); + + if (response.Images != null && response.Images.Count == 2) { + Debug.Log($"TestGenerateInlineImageWithConfig: Received expected 2 images."); + bool allImagesValid = true; + foreach(var image in response.Images) { + if (image.MimeType != "image/jpeg") { + Debug.LogError($"TestGenerateInlineImageWithConfig: Expected image/jpeg, got {image.MimeType}"); + allImagesValid = false; + } + Texture2D tex = image.AsTexture2D(); + if (tex == null || tex.width == 0 || tex.height == 0 ) { + Debug.LogError($"TestGenerateInlineImageWithConfig: AsTexture2D failed for an image."); + allImagesValid = false; + } else { + Debug.Log($"TestGenerateInlineImageWithConfig: Image {response.Images.IndexOf(image)+1} texture loaded: {tex.width}x{tex.height}"); + Object.Destroy(tex); + } + } + if (allImagesValid) Debug.Log("TestGenerateInlineImageWithConfig: PASS"); + else Debug.LogError("TestGenerateInlineImageWithConfig: FAIL (Issues with image properties or count, see logs)"); + + } else if (response.Images != null) { + Debug.LogError($"TestGenerateInlineImageWithConfig: FAIL - Expected 2 images, but got {response.Images.Count}. FilteredReason: {response.FilteredReason}"); + } else if (response.FilteredReason != null) { + Debug.LogWarning($"TestGenerateInlineImageWithConfig: No images received. FilteredReason: {response.FilteredReason}. This might be a PASS if the prompt was designed to be filtered."); + } + else { + Debug.LogError($"TestGenerateInlineImageWithConfig: FAIL - No images received and no FilteredReason provided."); + } + } catch (System.Exception e) { + Debug.LogError($"TestGenerateInlineImageWithConfig: FAILED with exception: {e}"); + } + } + + async Task TestGenerateInlineImageWithSafetySettings() { + Debug.Log("TestGenerateInlineImageWithSafetySettings: Starting..."); + if (ai == null) { + Debug.LogError("TestGenerateInlineImageWithSafetySettings: FirebaseAI not initialized."); + return; + } + + // Example: Block potentially sensitive content to a high degree + var safety = new ImagenSafetySettings( + safetyFilterLevel: ImagenSafetySettings.SafetyFilterLevel.BlockMediumAndAbove, + personFilterLevel: ImagenSafetySettings.PersonFilterLevel.BlockAll + ); + var model = ai.GetImagenModel(modelName: TestImagenModelName, safetySettings: safety); + // This prompt is neutral, but safety settings are applied. + // To properly test filtering, a prompt designed to be filtered would be needed, + // and then checking response.FilteredReason would be the main assertion. + var prompt = "A simple landscape with a house and a tree."; + + try { + var response = await model.GenerateImagesAsync(prompt: prompt); + + if (response.FilteredReason != null) { + Debug.LogWarning($"TestGenerateInlineImageWithSafetySettings: Images were filtered. Reason: {response.FilteredReason}. This is the expected outcome if the prompt triggered safety filters."); + Debug.Log("TestGenerateInlineImageWithSafetySettings: PASS (Filtered as potentially expected with strict settings)"); + } else if (response.Images != null && response.Images.Count > 0) { + Debug.Log($"TestGenerateInlineImageWithSafetySettings: Received {response.Images.Count} image(s). Prompt did not trigger strict safety filters or filters are not aggressive for this prompt."); + // This is also a valid outcome if the prompt is truly benign. + Debug.Log("TestGenerateInlineImageWithSafetySettings: PASS (Images generated, prompt was considered safe)"); + } else { + Debug.LogError($"TestGenerateInlineImageWithSafetySettings: FAIL - No images and no filtered reason."); + } + } catch (System.Exception e) { + Debug.LogError($"TestGenerateInlineImageWithSafetySettings: FAILED with exception: {e}"); + } + } + + async Task TestGenerateGcsImageSimple() { + Debug.Log("TestGenerateGcsImageSimple: Starting..."); + if (ai == null) { + Debug.LogError("TestGenerateGcsImageSimple: FirebaseAI not initialized."); + return; + } + + var model = ai.GetImagenModel(TestImagenModelName); + var prompt = "A detailed schematic of a futuristic spacecraft, GCS output."; + // Ensure TestGcsBucketPath ends with a '/' + var gcsUri = new System.Uri(TestGcsBucketPath + "gcs_image_test_" + System.DateTime.Now.Ticks + ".png"); + + try { + // This test relies on the service account having write permissions to the GCS bucket. + // In many automated test environments, this might be hard to guarantee or test directly. + // The primary check here is that the API call doesn't fail and returns a GCS URI. + var response = await model.GenerateImagesAsync(prompt: prompt, gcsUri: gcsUri); + + if (response.Images != null && response.Images.Count > 0) { + Debug.Log($"TestGenerateGcsImageSimple: Received {response.Images.Count} GCS image reference(s)."); + bool allUrisValid = true; + foreach (var image in response.Images) { + if (image.GcsUri != null && image.GcsUri.ToString().StartsWith("gs://")) { + Debug.Log($"TestGenerateGcsImageSimple: GCS Image URI is valid: {image.GcsUri} (MIME: {image.MimeType})."); + // Note: We can't easily verify the content of the GCS URI here without GCS client libs. + } else { + Debug.LogError("TestGenerateGcsImageSimple: GCS Image URI is null or invalid."); + allUrisValid = false; + } + } + if (allUrisValid) Debug.Log("TestGenerateGcsImageSimple: PASS (API call succeeded, GCS URIs look valid)"); + else Debug.LogError("TestGenerateGcsImageSimple: FAIL (One or more GCS URIs were invalid, see logs)"); + + } else if (response.FilteredReason != null) { + Debug.LogWarning($"TestGenerateGcsImageSimple: No GCS images generated. FilteredReason: {response.FilteredReason}."); + } + else { + Debug.LogError($"TestGenerateGcsImageSimple: FAIL - No GCS image references received and no FilteredReason provided."); + } + } catch (System.Exception e) { + // This could be due to various reasons: no permission to GCS bucket, invalid model, API errors. + Debug.LogError($"TestGenerateGcsImageSimple: FAILED with exception: {e}. " + + "Ensure the GCS bucket and path are correctly configured and the service account has write permissions."); + } + } + // Helper to ensure Firebase.Extensions.TaskExtension.ContinueWithOnMainThread is available + // This can be called in Start() if there are issues with ContinueWithOnMainThread. + void CheckExtensions() { + #if !FIREBASE_EXTENSIONS_PRESENT // Define this if you check for extensions explicitly + if (System.Type.GetType("Firebase.Extensions.TaskExtension, Firebase.TaskExtension") == null) { + Debug.LogError("Firebase.Extensions.TaskExtension not found. " + + "Please ensure Firebase Extensions (Firebase.TaskExtension.dll) is part of your project."); + } + #endif + } +}