package tencentvod import ( "bytes" "fmt" "io" "net/http" "strings" "time" "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/model" taskcommon "github.com/QuantumNous/new-api/relay/channel/task/taskcommon" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/service" "github.com/gin-gonic/gin" "github.com/pkg/errors" ) var ChannelName = "tencentcloud-vod-video" var ModelList = []string{"GV-3.1-fast"} type TaskAdaptor struct { ChannelType int apiKey string baseURL string } func (a *TaskAdaptor) Init(info *relaycommon.RelayInfo) { a.ChannelType = info.ChannelType a.baseURL = strings.TrimSpace(info.ChannelBaseUrl) a.apiKey = info.ApiKey } func (a *TaskAdaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycommon.RelayInfo) *dto.TaskError { return relaycommon.ValidateBasicTaskRequest(c, info, constant.TaskActionGenerate) } func (a *TaskAdaptor) EstimateBilling(_ *gin.Context, _ *relaycommon.RelayInfo) map[string]float64 { return nil } func (a *TaskAdaptor) AdjustBillingOnSubmit(_ *relaycommon.RelayInfo, _ []byte) map[string]float64 { return nil } func (a *TaskAdaptor) AdjustBillingOnComplete(_ *model.Task, _ *relaycommon.TaskInfo) int { return 0 } func (a *TaskAdaptor) BuildRequestURL(_ *relaycommon.RelayInfo) (string, error) { u := normalizeVodEndpoint(a.baseURL) return u + "/", nil } func (a *TaskAdaptor) BuildRequestHeader(_ *gin.Context, req *http.Request, _ *relaycommon.RelayInfo) error { req.Header.Set("Content-Type", "application/json") return nil } func (a *TaskAdaptor) BuildRequestBody(c *gin.Context, info *relaycommon.RelayInfo) (io.Reader, error) { req, err := relaycommon.GetTaskRequest(c) if err != nil { return nil, err } cred, err := ParseCredentials(a.apiKey) if err != nil { return nil, err } modelName, modelVersion := SplitCombinedModel(taskcommon.RelayTaskUpstreamModel(info, req.Model)) body := map[string]any{ "SubAppId": cred.SubAppID, "ModelName": modelName, "ModelVersion": modelVersion, } if prompt := strings.TrimSpace(req.GetPrompt()); prompt != "" { body["Prompt"] = prompt } fileInfos := make([]map[string]any, 0, 2) appendImageURL := func(url string) { u := strings.TrimSpace(url) if u == "" { return } fileInfos = append(fileInfos, map[string]any{ "Type": "Url", "Category": "Image", "Url": u, // 参考图生视频:显式标记为参考帧,避免被当作默认首帧。 "Usage": "Reference", }) } appendVideoURL := func(url string) { u := strings.TrimSpace(url) if u == "" { return } fileInfos = append(fileInfos, map[string]any{ "Type": "Url", "Category": "Video", "Url": u, "ReferenceType": "base", }) } // 图生视频:兼容 image 和 images[] 两种入参。 if img := strings.TrimSpace(req.Image); img != "" { appendImageURL(img) } for _, img := range req.Images { appendImageURL(img) } // 视频生视频:兼容 input_reference 入参。 if ref := strings.TrimSpace(req.InputReference); ref != "" { appendVideoURL(ref) } if len(fileInfos) > 0 { body["FileInfos"] = fileInfos } data, err := common.Marshal(body) if err != nil { return nil, err } return bytes.NewReader(data), nil } func normalizeVodEndpoint(raw string) string { u := strings.TrimRight(strings.TrimSpace(raw), "/") if u == "" { u = "https://vod.tencentcloudapi.com" } if !strings.HasPrefix(strings.ToLower(u), "http://") && !strings.HasPrefix(strings.ToLower(u), "https://") { u = "https://" + u } return u } func (a *TaskAdaptor) DoRequest(_ *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (*http.Response, error) { payload, err := io.ReadAll(requestBody) if err != nil { return nil, err } cred, err := ParseCredentials(info.ApiKey) if err != nil { return nil, err } return SignedPOSTJSON(strings.TrimSpace(info.ChannelSetting.Proxy), normalizeVodEndpoint(info.ChannelBaseUrl), cred.Region, cred, "CreateAigcVideoTask", payload) } func (a *TaskAdaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (string, []byte, *dto.TaskError) { respBody, err := io.ReadAll(resp.Body) if err != nil { return "", nil, service.TaskErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError) } _ = resp.Body.Close() var env struct { Response *struct { TaskId *string `json:"TaskId,omitempty"` Error *struct { Code string `json:"Code"` Message string `json:"Message"` } `json:"Error,omitempty"` } `json:"Response"` } if err = common.Unmarshal(respBody, &env); err != nil { return "", nil, service.TaskErrorWrapper(errors.Wrapf(err, "body: %s", respBody), "unmarshal_response_body_failed", http.StatusInternalServerError) } if env.Response != nil && env.Response.Error != nil && strings.TrimSpace(env.Response.Error.Message) != "" { return "", nil, service.TaskErrorWrapper(errors.New(env.Response.Error.Message), "video_submit_failed", http.StatusBadRequest) } taskID := "" if env.Response != nil && env.Response.TaskId != nil { taskID = strings.TrimSpace(*env.Response.TaskId) } if taskID == "" { return "", nil, service.TaskErrorWrapper(fmt.Errorf("task id is empty, body: %s", string(respBody)), "invalid_response", http.StatusInternalServerError) } ov := dto.NewOpenAIVideo() ov.ID = info.PublicTaskID ov.CreatedAt = dto.FormatTimeUnixRFC3339(time.Now().Unix()) ov.Model = info.OriginModelName c.JSON(http.StatusOK, ov) return taskID, respBody, nil } func (a *TaskAdaptor) FetchTask(baseURL, key string, body map[string]any, proxy string) (*http.Response, error) { taskID, _ := body["task_id"].(string) taskID = strings.TrimSpace(taskID) if taskID == "" { return nil, fmt.Errorf("invalid task_id") } cred, err := ParseCredentials(key) if err != nil { return nil, err } payload, err := common.Marshal(map[string]any{"TaskId": taskID, "SubAppId": cred.SubAppID}) if err != nil { return nil, err } return SignedPOSTJSON(strings.TrimSpace(proxy), normalizeVodEndpoint(baseURL), cred.Region, cred, "DescribeTaskDetail", payload) } func (a *TaskAdaptor) ParseTaskResult(respBody []byte) (*relaycommon.TaskInfo, error) { var env struct { Response *struct { Status *string `json:"Status,omitempty"` AigcVideoTask *struct { Output *struct { FileInfos []struct { FileUrl *string `json:"FileUrl,omitempty"` } `json:"FileInfos,omitempty"` } `json:"Output,omitempty"` Message *string `json:"Message,omitempty"` } `json:"AigcVideoTask,omitempty"` } `json:"Response"` } if err := common.Unmarshal(respBody, &env); err != nil { return nil, err } ti := &relaycommon.TaskInfo{Code: 0, Status: string(model.TaskStatusInProgress), Progress: "0%"} if env.Response == nil || env.Response.Status == nil { return ti, nil } switch strings.ToUpper(strings.TrimSpace(*env.Response.Status)) { case "FINISH": if env.Response.AigcVideoTask != nil && env.Response.AigcVideoTask.Output != nil { for _, fi := range env.Response.AigcVideoTask.Output.FileInfos { if fi.FileUrl != nil && strings.TrimSpace(*fi.FileUrl) != "" { ti.Status = string(model.TaskStatusSuccess) ti.Progress = "100%" ti.Url = strings.TrimSpace(*fi.FileUrl) return ti, nil } } } ti.Status = string(model.TaskStatusFailure) ti.Progress = "100%" case "ABORTED": ti.Status = string(model.TaskStatusFailure) ti.Progress = "100%" } return ti, nil } func (a *TaskAdaptor) GetModelList() []string { return ModelList } func (a *TaskAdaptor) GetChannelName() string { return ChannelName } func (a *TaskAdaptor) ConvertToOpenAIVideo(originTask *model.Task) ([]byte, error) { ov := originTask.ToOpenAIVideo() var env struct { Response *struct { Error *struct { Code string `json:"Code,omitempty"` Message string `json:"Message,omitempty"` } `json:"Error,omitempty"` AigcVideoTask *struct { Message *string `json:"Message,omitempty"` Output *struct { FileInfos []struct { FileUrl *string `json:"FileUrl,omitempty"` } `json:"FileInfos,omitempty"` } `json:"Output,omitempty"` } `json:"AigcVideoTask,omitempty"` } `json:"Response,omitempty"` } if err := common.Unmarshal(originTask.Data, &env); err == nil && env.Response != nil { if env.Response.Error != nil && strings.TrimSpace(env.Response.Error.Message) != "" { ov.Error = &dto.OpenAIVideoError{Message: strings.TrimSpace(env.Response.Error.Message), Code: strings.TrimSpace(env.Response.Error.Code)} } if env.Response.AigcVideoTask != nil && env.Response.AigcVideoTask.Output != nil { for _, fi := range env.Response.AigcVideoTask.Output.FileInfos { if fi.FileUrl != nil && strings.TrimSpace(*fi.FileUrl) != "" { ov.SetMetadata("url", strings.TrimSpace(*fi.FileUrl)) break } } } if ov.Error == nil && originTask.Status == model.TaskStatusFailure { msg := strings.TrimSpace(originTask.FailReason) if env.Response.AigcVideoTask != nil && env.Response.AigcVideoTask.Message != nil && strings.TrimSpace(*env.Response.AigcVideoTask.Message) != "" { msg = strings.TrimSpace(*env.Response.AigcVideoTask.Message) } if msg != "" { ov.Error = &dto.OpenAIVideoError{Message: msg, Code: "tencent_vod_task_failed"} } } } return common.Marshal(ov) }