188 lines
5.0 KiB
Go
188 lines
5.0 KiB
Go
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
|
|
}
|