persona-community-3/pkg/gemini/video.go
jordan f53b908499
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
ci/woodpecker/manual/woodpecker Pipeline was successful
Initialize project from skeleton template
2026-02-23 11:10:35 +00:00

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
}