package gemini import ( "context" "fmt" "time" "google.golang.org/genai" ) const ( // Veo models ModelVeo31 = "veo-3.1-generate-preview" ModelVeo2 = "veo-2.0-generate-001" defaultVideoModel = ModelVeo31 ) // VideoRequest represents a video generation request type VideoRequest struct { Model string // Model to use (default: "veo-3.1-generate-preview") Prompt string // Required: text description of the desired video Image []byte // Optional: reference image for image-to-video ImageMimeType string // Optional: MIME type of the reference image (default: "image/png") AspectRatio string // Optional: aspect ratio (e.g., "16:9", "9:16") Duration string // Optional: video duration (e.g., "5s", "10s") } // VideoResponse represents a video generation response type VideoResponse struct { Video VideoData // Generated video } // VideoData represents a single generated video type VideoData struct { Data []byte // Raw video bytes MimeType string // MIME type of the video URI string // URI if available (for downloading) } // GenerateVideo generates a video using the Veo model // Note: Video generation is asynchronous and this method polls until completion func (c *Client) GenerateVideo(ctx context.Context, req VideoRequest) (*VideoResponse, error) { // Validate required fields if req.Prompt == "" { return nil, fmt.Errorf("%w: prompt is required", ErrInvalidConfig) } // Set defaults if req.Model == "" { req.Model = defaultVideoModel } c.logger.Debug("starting video generation", "model", req.Model, "prompt_length", len(req.Prompt), "has_image", len(req.Image) > 0, ) // Build configuration var config *genai.GenerateVideosConfig if req.AspectRatio != "" || req.Duration != "" { config = &genai.GenerateVideosConfig{} if req.AspectRatio != "" { config.AspectRatio = req.AspectRatio } // Duration would be set here if the SDK supports it } // Prepare image input if provided var image *genai.Image if len(req.Image) > 0 { mimeType := req.ImageMimeType if mimeType == "" { mimeType = "image/png" } image = &genai.Image{ ImageBytes: req.Image, MIMEType: mimeType, } } // Start video generation (async operation) with retry var operation *genai.GenerateVideosOperation err := c.retryWithBackoff(ctx, "GenerateVideo", func() error { var apiErr error operation, apiErr = c.genaiClient.Models.GenerateVideos(ctx, req.Model, req.Prompt, image, config) if apiErr != nil { return classifyError(apiErr) } return nil }) if err != nil { return nil, err } c.logger.Debug("video generation started, polling for completion", "operation_name", operation.Name, ) // Poll for completion startTime := time.Now() for !operation.Done { // Check timeout if time.Since(startTime) > c.config.VideoMaxWait { return nil, fmt.Errorf("%w: video generation timed out after %v", ErrTimeout, c.config.VideoMaxWait) } c.logger.Debug("waiting for video generation", "elapsed", time.Since(startTime).Round(time.Second), ) // Wait before polling again select { case <-ctx.Done(): return nil, ctx.Err() case <-time.After(c.config.VideoPollDelay): } // Get updated operation status with retry err = c.retryWithBackoff(ctx, "GetVideosOperation", func() error { var apiErr error operation, apiErr = c.genaiClient.Operations.GetVideosOperation(ctx, operation, nil) if apiErr != nil { return classifyError(apiErr) } return nil }) if err != nil { return nil, err } } c.logger.Debug("video generation complete", "elapsed", time.Since(startTime).Round(time.Second), ) // Check for errors in the operation if operation.Error != nil { msg, _ := operation.Error["message"].(string) code, _ := operation.Error["code"].(float64) return nil, fmt.Errorf("video generation failed: %s (code: %.0f)", msg, code) } if operation.Response == nil { return nil, fmt.Errorf("no video generated (nil response)") } if len(operation.Response.GeneratedVideos) == 0 { // Log the full response for debugging c.logger.Error("video generation returned empty", "response", fmt.Sprintf("%+v", operation.Response), ) return nil, fmt.Errorf("no video generated (empty GeneratedVideos array)") } // Get the generated video generatedVideo := operation.Response.GeneratedVideos[0] if generatedVideo.Video == nil { return nil, fmt.Errorf("video data is empty") } videoResp := &VideoResponse{ Video: VideoData{ URI: generatedVideo.Video.URI, }, } // Download the video if URI is provided if generatedVideo.Video.URI != "" { c.logger.Debug("downloading video", "uri", generatedVideo.Video.URI, ) downloadResp, err := c.genaiClient.Files.Download(ctx, generatedVideo.Video, nil) if err != nil { // Return URI even if download fails c.logger.Warn("failed to download video, returning URI only", "error", err, ) return videoResp, nil } videoResp.Video.Data = downloadResp videoResp.Video.MimeType = "video/mp4" } return videoResp, nil }