Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 28 additions & 62 deletions relay/channel/task/gemini/adaptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,31 +24,29 @@ import (
"github.com/pkg/errors"
)

// VideoGenerationConfig represents the video generation configuration
// ============================
// Request / Response structures
// ============================

// GeminiVideoGenerationConfig represents the video generation configuration
// Based on: https://ai.google.dev/gemini-api/docs/video
type VideoGenerationConfig struct {
type GeminiVideoGenerationConfig struct {
AspectRatio string `json:"aspectRatio,omitempty"` // "16:9" or "9:16"
DurationSeconds float64 `json:"durationSeconds,omitempty"` // 4, 6, or 8 (as number)
NegativePrompt string `json:"negativePrompt,omitempty"` // unwanted elements
PersonGeneration string `json:"personGeneration,omitempty"` // "allow_all" for text-to-video, "allow_adult" for image-to-video
Resolution string `json:"resolution,omitempty"` // video resolution
}

type Image struct {
BytesBase64Encoded string `json:"bytesBase64Encoded,omitempty"`
MimeType string `json:"mimeType,omitempty"`
// GeminiVideoRequest represents a single video generation instance
type GeminiVideoRequest struct {
Prompt string `json:"prompt"`
}

type VideoRequest struct {
Prompt string `json:"prompt"`
Image *Image `json:"image,omitempty"`
LastFrame *Image `json:"lastFrame,omitempty"`
}

// VideoPayload represents the complete video generation request payload
type VideoPayload struct {
Instances []VideoRequest `json:"instances"`
Parameters VideoGenerationConfig `json:"parameters,omitempty"`
// GeminiVideoPayload represents the complete video generation request payload
type GeminiVideoPayload struct {
Instances []GeminiVideoRequest `json:"instances"`
Parameters GeminiVideoGenerationConfig `json:"parameters,omitempty"`
}

type submitResponse struct {
Expand Down Expand Up @@ -77,8 +75,6 @@ type operationResponse struct {
URI string `json:"uri"`
} `json:"video"`
} `json:"generatedSamples"`
RaiMediaFilteredCount int `json:"raiMediaFilteredCount"`
RaiMediaFilteredReasons []string `json:"raiMediaFilteredReasons"`
} `json:"generateVideoResponse"`
} `json:"response"`
Error struct {
Expand All @@ -104,7 +100,8 @@ func (a *TaskAdaptor) Init(info *relaycommon.RelayInfo) {

// ValidateRequestAndSetAction parses body, validates fields and sets default action.
func (a *TaskAdaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycommon.RelayInfo) (taskErr *dto.TaskError) {
return relaycommon.ValidateBasicTaskRequest(c, info, constant.TaskActionGenerate)
// Use the standard validation method for TaskSubmitReq
return relaycommon.ValidateBasicTaskRequest(c, info, constant.TaskActionTextGenerate)
}

// BuildRequestURL constructs the upstream URL.
Expand Down Expand Up @@ -140,21 +137,13 @@ func (a *TaskAdaptor) BuildRequestBody(c *gin.Context, info *relaycommon.RelayIn
}

// Create structured video generation request
body := VideoPayload{
Instances: []VideoRequest{
body := GeminiVideoPayload{
Instances: []GeminiVideoRequest{
{Prompt: req.Prompt},
},
Parameters: VideoGenerationConfig{},
}

if len(req.Images) > 0 {
body.Instances[0].Image = a.convertImage(req.Images[0])
}
if len(req.Images) > 1 {
body.Instances[0].LastFrame = a.convertImage(req.Images[1])
Parameters: GeminiVideoGenerationConfig{},
}

// Parse metadata for additional configuration
metadata := req.Metadata
medaBytes, err := json.Marshal(metadata)
if err != nil {
Expand Down Expand Up @@ -258,19 +247,20 @@ func (a *TaskAdaptor) ParseTaskResult(respBody []byte) (*relaycommon.TaskInfo, e
return ti, nil
}

if len(op.Response.GenerateVideoResponse.GeneratedSamples) == 0 {
ti.Status = model.TaskStatusFailure
ti.Reason = fmt.Sprintf("no generated video url found: %s", strings.Join(op.Response.GenerateVideoResponse.RaiMediaFilteredReasons, "; "))
} else {
if uri := op.Response.GenerateVideoResponse.GeneratedSamples[0].Video.URI; uri != "" {
ti.RemoteUrl = uri
}
ti.Status = model.TaskStatusSuccess
}
ti.Status = model.TaskStatusSuccess
ti.Progress = "100%"

taskID := encodeLocalTaskID(op.Name)
ti.TaskID = taskID
ti.Url = fmt.Sprintf("%s/v1/videos/%s/content", system_setting.ServerAddress, taskID)

// Extract URL from generateVideoResponse if available
if len(op.Response.GenerateVideoResponse.GeneratedSamples) > 0 {
if uri := op.Response.GenerateVideoResponse.GeneratedSamples[0].Video.URI; uri != "" {
ti.RemoteUrl = uri
}
}

return ti, nil
}

Expand Down Expand Up @@ -299,30 +289,6 @@ func (a *TaskAdaptor) ConvertToOpenAIVideo(task *model.Task) ([]byte, error) {
return common.Marshal(video)
}

func (a *TaskAdaptor) convertImage(imageStr string) *Image {
if strings.TrimSpace(imageStr) == "" {
return nil
}
img := &Image{
MimeType: "image/png",
BytesBase64Encoded: imageStr,
}
if strings.HasPrefix(imageStr, "data:image/") {
parts := strings.Split(imageStr, ";base64,")
if len(parts) == 2 {
img.MimeType = strings.TrimPrefix(parts[0], "data:")
img.BytesBase64Encoded = parts[1]
}
} else if strings.HasPrefix(imageStr, "http") {
mimeType, data, err := service.GetImageFromUrl(imageStr)
if err == nil {
img.MimeType = mimeType
img.BytesBase64Encoded = data
}
}
return img
}

// ============================
// helpers
// ============================
Expand Down