persona-community-5/pkg/gemini/image.go
jordan bd2f591b98
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
ci/woodpecker/manual/woodpecker Pipeline was successful
Initialize project from skeleton template
2026-02-24 07:39:46 +00:00

182 lines
4.7 KiB
Go

package gemini
import (
"context"
"crypto/rand"
"encoding/binary"
"fmt"
"google.golang.org/genai"
)
const (
// Gemini API seed range constraints
minSeed = 1
maxSeed = 999999
)
const (
// Gemini native image models (Nano Banana Pro)
ModelGemini3ProImage = "gemini-3-pro-image-preview"
defaultImageModel = ModelGemini3ProImage
)
// ImageRequest represents an image generation request
type ImageRequest struct {
Model string // Model to use (default: "gemini-3-pro-image-preview")
Prompt string // Required: text description of the desired image
AspectRatio string // Optional: aspect ratio (e.g., "16:9", "1:1", "9:16")
Size string // Optional: image size - "1K", "2K", "4K" (Gemini 3 Pro only)
// Reference image for identity consistency
ReferenceImage []byte // Optional: reference image bytes
ReferenceMime string // Optional: MIME type ("image/png", "image/webp")
// Determinism controls
Seed *int32 // Optional: seed for reproducible results (nil = random)
}
// ImageResponse represents an image generation response
type ImageResponse struct {
Images []ImageData // List of generated images
Text string // Optional text response from model
Seed *int32 // Seed used for generation
}
// ImageData represents a single generated image
type ImageData struct {
Data []byte // Raw image bytes
MimeType string // MIME type of the image
}
// GenerateImage generates images using Gemini native image generation (Nano Banana Pro)
func (c *Client) GenerateImage(ctx context.Context, req ImageRequest) (*ImageResponse, error) {
// Validate required fields
if req.Prompt == "" {
return nil, fmt.Errorf("%w: prompt is required", ErrInvalidConfig)
}
// Set defaults
if req.Model == "" {
req.Model = defaultImageModel
}
// Generate or validate provided seed
var seed int32
if req.Seed != nil {
seed = *req.Seed
// Validate seed is within Gemini's accepted range
if seed < minSeed || seed > maxSeed {
return nil, fmt.Errorf("%w: seed must be between %d and %d (got %d)",
ErrInvalidConfig, minSeed, maxSeed, seed)
}
} else {
// Generate cryptographically random seed
var seedBytes [4]byte
if _, err := rand.Read(seedBytes[:]); err != nil {
return nil, fmt.Errorf("generate random seed: %w", err)
}
seed = int32(binary.LittleEndian.Uint32(seedBytes[:])%maxSeed) + minSeed
}
c.logger.Debug("generating image",
"model", req.Model,
"prompt_length", len(req.Prompt),
"seed", seed,
"has_reference", len(req.ReferenceImage) > 0,
)
// Build generation config
config := &genai.GenerateContentConfig{
ResponseModalities: []string{"image", "text"},
Seed: &seed,
}
// Apply image-specific config (aspect ratio, size)
if req.AspectRatio != "" || req.Size != "" {
config.ImageConfig = &genai.ImageConfig{}
if req.AspectRatio != "" {
config.ImageConfig.AspectRatio = req.AspectRatio
}
if req.Size != "" {
config.ImageConfig.ImageSize = req.Size
}
}
// Build content parts based on whether reference image is provided
var parts []*genai.Part
if len(req.ReferenceImage) > 0 {
// Determine MIME type
mime := req.ReferenceMime
if mime == "" {
mime = "image/png" // default
}
// Multipart content: reference image + text prompt
parts = []*genai.Part{
{InlineData: &genai.Blob{MIMEType: mime, Data: req.ReferenceImage}},
{Text: req.Prompt},
}
} else {
// Text-only content
parts = []*genai.Part{
{Text: req.Prompt},
}
}
content := []*genai.Content{{Parts: parts}}
// Call the API with retry for transient errors
var response *genai.GenerateContentResponse
err := c.retryWithBackoff(ctx, "GenerateImage", func() error {
var apiErr error
response, apiErr = c.genaiClient.Models.GenerateContent(ctx, req.Model, content, config)
if apiErr != nil {
return classifyError(apiErr)
}
return nil
})
if err != nil {
return nil, err
}
// Convert response
imageResp := &ImageResponse{
Images: make([]ImageData, 0),
Seed: &seed,
}
// Extract images and text from response
if response != nil && len(response.Candidates) > 0 {
candidate := response.Candidates[0]
if candidate.Content != nil {
for _, part := range candidate.Content.Parts {
if part.Text != "" {
imageResp.Text = part.Text
}
if part.InlineData != nil && len(part.InlineData.Data) > 0 {
mimeType := "image/png"
if part.InlineData.MIMEType != "" {
mimeType = part.InlineData.MIMEType
}
imageResp.Images = append(imageResp.Images, ImageData{
Data: part.InlineData.Data,
MimeType: mimeType,
})
}
}
}
}
if len(imageResp.Images) == 0 {
return nil, fmt.Errorf("no images generated")
}
c.logger.Debug("image generation complete",
"images_count", len(imageResp.Images),
)
return imageResp, nil
}