From 4e69c98b4222f7a7862e58b880adecff0e302247 Mon Sep 17 00:00:00 2001 From: Seefs <40468931+seefs001@users.noreply.github.com> Date: Thu, 11 Dec 2025 23:35:23 +0800 Subject: [PATCH] Merge pull request #2412 from seefs001/pr-2372 feat: add openai video remix endpoint --- constant/task.go | 1 + middleware/distributor.go | 4 + relay/channel/task/sora/adaptor.go | 21 ++++ relay/relay_task.go | 115 +++++++++++++----- router/video-router.go | 1 + .../table/task-logs/TaskLogsColumnDefs.jsx | 10 +- web/src/constants/common.constant.js | 1 + web/src/i18n/locales/en.json | 1 + web/src/i18n/locales/fr.json | 1 + web/src/i18n/locales/ja.json | 1 + web/src/i18n/locales/ru.json | 1 + web/src/i18n/locales/vi.json | 1 + web/src/i18n/locales/zh.json | 1 + 13 files changed, 130 insertions(+), 29 deletions(-) diff --git a/constant/task.go b/constant/task.go index e174fd60e..ecccf4dfe 100644 --- a/constant/task.go +++ b/constant/task.go @@ -15,6 +15,7 @@ const ( TaskActionTextGenerate = "textGenerate" TaskActionFirstTailGenerate = "firstTailGenerate" TaskActionReferenceGenerate = "referenceGenerate" + TaskActionRemix = "remixGenerate" ) var SunoModel2Action = map[string]string{ diff --git a/middleware/distributor.go b/middleware/distributor.go index 5a9deb23c..3c8529d96 100644 --- a/middleware/distributor.go +++ b/middleware/distributor.go @@ -181,6 +181,10 @@ func getModelRequest(c *gin.Context) (*ModelRequest, bool, error) { } c.Set("platform", string(constant.TaskPlatformSuno)) c.Set("relay_mode", relayMode) + } else if strings.Contains(c.Request.URL.Path, "/v1/videos/") && strings.HasSuffix(c.Request.URL.Path, "/remix") { + relayMode := relayconstant.RelayModeVideoSubmit + c.Set("relay_mode", relayMode) + shouldSelectChannel = false } else if strings.Contains(c.Request.URL.Path, "/v1/videos") { //curl https://api.openai.com/v1/videos \ // -H "Authorization: Bearer $OPENAI_API_KEY" \ diff --git a/relay/channel/task/sora/adaptor.go b/relay/channel/task/sora/adaptor.go index 214561b5b..9dc03796c 100644 --- a/relay/channel/task/sora/adaptor.go +++ b/relay/channel/task/sora/adaptor.go @@ -5,8 +5,10 @@ import ( "fmt" "io" "net/http" + "strings" "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/constant" "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/relay/channel" @@ -67,11 +69,30 @@ func (a *TaskAdaptor) Init(info *relaycommon.RelayInfo) { a.apiKey = info.ApiKey } +func validateRemixRequest(c *gin.Context) *dto.TaskError { + var req struct { + Prompt string `json:"prompt"` + } + if err := common.UnmarshalBodyReusable(c, &req); err != nil { + return service.TaskErrorWrapperLocal(err, "invalid_request", http.StatusBadRequest) + } + if strings.TrimSpace(req.Prompt) == "" { + return service.TaskErrorWrapperLocal(fmt.Errorf("field prompt is required"), "invalid_request", http.StatusBadRequest) + } + return nil +} + func (a *TaskAdaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycommon.RelayInfo) (taskErr *dto.TaskError) { + if info.Action == constant.TaskActionRemix { + return validateRemixRequest(c) + } return relaycommon.ValidateMultipartDirect(c, info) } func (a *TaskAdaptor) BuildRequestURL(info *relaycommon.RelayInfo) (string, error) { + if info.Action == constant.TaskActionRemix { + return fmt.Sprintf("%s/v1/videos/%s/remix", a.baseURL, info.OriginTaskID), nil + } return fmt.Sprintf("%s/v1/videos", a.baseURL), nil } diff --git a/relay/relay_task.go b/relay/relay_task.go index ba9fe1e8f..bac05e0ee 100644 --- a/relay/relay_task.go +++ b/relay/relay_task.go @@ -32,7 +32,94 @@ func RelayTaskSubmit(c *gin.Context, info *relaycommon.RelayInfo) (taskErr *dto. if info.TaskRelayInfo == nil { info.TaskRelayInfo = &relaycommon.TaskRelayInfo{} } + path := c.Request.URL.Path + if strings.Contains(path, "/v1/videos/") && strings.HasSuffix(path, "/remix") { + info.Action = constant.TaskActionRemix + } + + // 提取 remix 任务的 video_id + if info.Action == constant.TaskActionRemix { + videoID := c.Param("video_id") + if strings.TrimSpace(videoID) == "" { + return service.TaskErrorWrapperLocal(fmt.Errorf("video_id is required"), "invalid_request", http.StatusBadRequest) + } + info.OriginTaskID = videoID + } + platform := constant.TaskPlatform(c.GetString("platform")) + + // 获取原始任务信息 + if info.OriginTaskID != "" { + originTask, exist, err := model.GetByTaskId(info.UserId, info.OriginTaskID) + if err != nil { + taskErr = service.TaskErrorWrapper(err, "get_origin_task_failed", http.StatusInternalServerError) + return + } + if !exist { + taskErr = service.TaskErrorWrapperLocal(errors.New("task_origin_not_exist"), "task_not_exist", http.StatusBadRequest) + return + } + if info.OriginModelName == "" { + if originTask.Properties.OriginModelName != "" { + info.OriginModelName = originTask.Properties.OriginModelName + } else if originTask.Properties.UpstreamModelName != "" { + info.OriginModelName = originTask.Properties.UpstreamModelName + } else { + var taskData map[string]interface{} + _ = json.Unmarshal(originTask.Data, &taskData) + if m, ok := taskData["model"].(string); ok && m != "" { + info.OriginModelName = m + platform = originTask.Platform + } + } + } + if originTask.ChannelId != info.ChannelId { + channel, err := model.GetChannelById(originTask.ChannelId, true) + if err != nil { + taskErr = service.TaskErrorWrapperLocal(err, "channel_not_found", http.StatusBadRequest) + return + } + if channel.Status != common.ChannelStatusEnabled { + taskErr = service.TaskErrorWrapperLocal(errors.New("the channel of the origin task is disabled"), "task_channel_disable", http.StatusBadRequest) + return + } + key, _, newAPIError := channel.GetNextEnabledKey() + if newAPIError != nil { + taskErr = service.TaskErrorWrapper(newAPIError, "channel_no_available_key", newAPIError.StatusCode) + return + } + common.SetContextKey(c, constant.ContextKeyChannelKey, key) + common.SetContextKey(c, constant.ContextKeyChannelType, channel.Type) + common.SetContextKey(c, constant.ContextKeyChannelBaseUrl, channel.GetBaseURL()) + common.SetContextKey(c, constant.ContextKeyChannelId, originTask.ChannelId) + + info.ChannelBaseUrl = channel.GetBaseURL() + info.ChannelId = originTask.ChannelId + info.ChannelType = channel.Type + info.ApiKey = key + platform = originTask.Platform + } + + // 使用原始任务的参数 + if info.Action == constant.TaskActionRemix { + var taskData map[string]interface{} + _ = json.Unmarshal(originTask.Data, &taskData) + secondsStr, _ := taskData["seconds"].(string) + seconds, _ := strconv.Atoi(secondsStr) + if seconds <= 0 { + seconds = 4 + } + sizeStr, _ := taskData["size"].(string) + if info.PriceData.OtherRatios == nil { + info.PriceData.OtherRatios = map[string]float64{} + } + info.PriceData.OtherRatios["seconds"] = float64(seconds) + info.PriceData.OtherRatios["size"] = 1 + if sizeStr == "1792x1024" || sizeStr == "1024x1792" { + info.PriceData.OtherRatios["size"] = 1.666667 + } + } + } if platform == "" { platform = GetTaskPlatform(c) } @@ -94,34 +181,6 @@ func RelayTaskSubmit(c *gin.Context, info *relaycommon.RelayInfo) (taskErr *dto. return } - if info.OriginTaskID != "" { - originTask, exist, err := model.GetByTaskId(info.UserId, info.OriginTaskID) - if err != nil { - taskErr = service.TaskErrorWrapper(err, "get_origin_task_failed", http.StatusInternalServerError) - return - } - if !exist { - taskErr = service.TaskErrorWrapperLocal(errors.New("task_origin_not_exist"), "task_not_exist", http.StatusBadRequest) - return - } - if originTask.ChannelId != info.ChannelId { - channel, err := model.GetChannelById(originTask.ChannelId, true) - if err != nil { - taskErr = service.TaskErrorWrapperLocal(err, "channel_not_found", http.StatusBadRequest) - return - } - if channel.Status != common.ChannelStatusEnabled { - return service.TaskErrorWrapperLocal(errors.New("该任务所属渠道已被禁用"), "task_channel_disable", http.StatusBadRequest) - } - c.Set("base_url", channel.GetBaseURL()) - c.Set("channel_id", originTask.ChannelId) - c.Request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", channel.Key)) - - info.ChannelBaseUrl = channel.GetBaseURL() - info.ChannelId = originTask.ChannelId - } - } - // build body requestBody, err := adaptor.BuildRequestBody(c, info) if err != nil { diff --git a/router/video-router.go b/router/video-router.go index 87097cf86..d5fed1d78 100644 --- a/router/video-router.go +++ b/router/video-router.go @@ -14,6 +14,7 @@ func SetVideoRouter(router *gin.Engine) { videoV1Router.GET("/videos/:task_id/content", controller.VideoProxy) videoV1Router.POST("/video/generations", controller.RelayTask) videoV1Router.GET("/video/generations/:task_id", controller.RelayTask) + videoV1Router.POST("/videos/:video_id/remix", controller.RelayTask) } // openai compatible API video routes // docs: https://platform.openai.com/docs/api-reference/videos/create diff --git a/web/src/components/table/task-logs/TaskLogsColumnDefs.jsx b/web/src/components/table/task-logs/TaskLogsColumnDefs.jsx index 530518d18..969977d17 100644 --- a/web/src/components/table/task-logs/TaskLogsColumnDefs.jsx +++ b/web/src/components/table/task-logs/TaskLogsColumnDefs.jsx @@ -39,6 +39,7 @@ import { TASK_ACTION_GENERATE, TASK_ACTION_REFERENCE_GENERATE, TASK_ACTION_TEXT_GENERATE, + TASK_ACTION_REMIX_GENERATE, } from '../../../constants/common.constant'; import { CHANNEL_OPTIONS } from '../../../constants/channel.constants'; @@ -125,6 +126,12 @@ const renderType = (type, t) => { {t('参照生视频')} ); + case TASK_ACTION_REMIX_GENERATE: + return ( + }> + {t('视频Remix')} + + ); default: return ( }> @@ -359,7 +366,8 @@ export const getTaskLogsColumns = ({ record.action === TASK_ACTION_GENERATE || record.action === TASK_ACTION_TEXT_GENERATE || record.action === TASK_ACTION_FIRST_TAIL_GENERATE || - record.action === TASK_ACTION_REFERENCE_GENERATE; + record.action === TASK_ACTION_REFERENCE_GENERATE || + record.action === TASK_ACTION_REMIX_GENERATE; const isSuccess = record.status === 'SUCCESS'; const isUrl = typeof text === 'string' && /^https?:\/\//.test(text); if (isSuccess && isVideoTask && isUrl) { diff --git a/web/src/constants/common.constant.js b/web/src/constants/common.constant.js index 57fbbbde5..a142a0eb5 100644 --- a/web/src/constants/common.constant.js +++ b/web/src/constants/common.constant.js @@ -42,3 +42,4 @@ export const TASK_ACTION_GENERATE = 'generate'; export const TASK_ACTION_TEXT_GENERATE = 'textGenerate'; export const TASK_ACTION_FIRST_TAIL_GENERATE = 'firstTailGenerate'; export const TASK_ACTION_REFERENCE_GENERATE = 'referenceGenerate'; +export const TASK_ACTION_REMIX_GENERATE = 'remixGenerate'; diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index 3f279e13a..efdb89a59 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -548,6 +548,7 @@ "参数值": "Parameter value", "参数覆盖": "Parameters override", "参照生视频": "Reference video generation", + "视频Remix": "Video remix", "友情链接": "Friendly links", "发布日期": "Publish Date", "发布时间": "Publish Time", diff --git a/web/src/i18n/locales/fr.json b/web/src/i18n/locales/fr.json index ed1df8a83..c10229e48 100644 --- a/web/src/i18n/locales/fr.json +++ b/web/src/i18n/locales/fr.json @@ -551,6 +551,7 @@ "参数值": "Valeur du paramètre", "参数覆盖": "Remplacement des paramètres", "参照生视频": "Générer une vidéo par référence", + "视频Remix": "Remix vidéo", "友情链接": "Liens amicaux", "发布日期": "Date de publication", "发布时间": "Heure de publication", diff --git a/web/src/i18n/locales/ja.json b/web/src/i18n/locales/ja.json index 0e4786c68..b67f5529e 100644 --- a/web/src/i18n/locales/ja.json +++ b/web/src/i18n/locales/ja.json @@ -510,6 +510,7 @@ "参数值": "パラメータ値", "参数覆盖": "パラメータの上書き", "参照生视频": "参照動画生成", + "视频Remix": "動画リミックス", "友情链接": "関連リンク", "发布日期": "公開日", "发布时间": "公開日時", diff --git a/web/src/i18n/locales/ru.json b/web/src/i18n/locales/ru.json index 92171a0c3..eb13f6106 100644 --- a/web/src/i18n/locales/ru.json +++ b/web/src/i18n/locales/ru.json @@ -555,6 +555,7 @@ "参数值": "Значение параметра", "参数覆盖": "Переопределение параметров", "参照生视频": "Ссылка на генерацию видео", + "视频Remix": "Видео ремикс", "友情链接": "Дружественные ссылки", "发布日期": "Дата публикации", "发布时间": "Время публикации", diff --git a/web/src/i18n/locales/vi.json b/web/src/i18n/locales/vi.json index 8af562f7a..39b80674d 100644 --- a/web/src/i18n/locales/vi.json +++ b/web/src/i18n/locales/vi.json @@ -510,6 +510,7 @@ "参数值": "Giá trị tham số", "参数覆盖": "Ghi đè tham số", "参照生视频": "Tạo video tham chiếu", + "视频Remix": "Remix video", "友情链接": "Liên kết thân thiện", "发布日期": "Ngày xuất bản", "发布时间": "Thời gian xuất bản", diff --git a/web/src/i18n/locales/zh.json b/web/src/i18n/locales/zh.json index a07885638..d3441f3b5 100644 --- a/web/src/i18n/locales/zh.json +++ b/web/src/i18n/locales/zh.json @@ -543,6 +543,7 @@ "参数值": "参数值", "参数覆盖": "参数覆盖", "参照生视频": "参照生视频", + "视频Remix": "视频 Remix", "友情链接": "友情链接", "发布日期": "发布日期", "发布时间": "发布时间",