diff --git a/ruoyi-admin/src/main/java/com/ruoyi/api/PortalVideoController.java b/ruoyi-admin/src/main/java/com/ruoyi/api/PortalVideoController.java index df44409..af49924 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/api/PortalVideoController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/api/PortalVideoController.java @@ -15,11 +15,13 @@ import com.ruoyi.ai.service.IByteDeptApiKeyService; import com.ruoyi.ai.service.IByteService; import com.ruoyi.api.request.PortalVideoGenRequest; import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.core.page.TableDataInfo; import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.TencentCosUtil; +import com.ruoyi.config.PortalVideoProperties; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.RequiredArgsConstructor; @@ -28,7 +30,9 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.*; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -45,11 +49,11 @@ public class PortalVideoController extends BaseController { private final IByteDeptApiKeyService byteDeptApiKeyService; private final IAiOrderService aiOrderService; private final TencentCosUtil tencentCosUtil; + private final PortalVideoProperties portalVideoProperties; @Value("${volcengine.ark.callbackUrl:}") private String volcCallbackUrl; - private static final String DEFAULT_MODEL = "ep-20260326165811-dlkth"; private static final ObjectMapper OM = new ObjectMapper(); private String apiKey() { @@ -57,17 +61,22 @@ public class PortalVideoController extends BaseController { } private void applyOptionalParams(ByteBodyReq body, PortalVideoGenRequest req) { - body.setDuration(req.getDuration() != null ? req.getDuration() : 4); - body.setResolution(StringUtils.isNotEmpty(req.getResolution()) ? req.getResolution() : "720p"); - body.setRatio(StringUtils.isNotEmpty(req.getRatio()) ? req.getRatio() : "3:4"); + PortalVideoProperties.Defaults d = portalVideoProperties.getDefaults(); + body.setDuration(req.getDuration() != null ? req.getDuration() : d.getDuration()); + body.setResolution(StringUtils.isNotEmpty(req.getResolution()) ? req.getResolution() : d.getResolution()); + body.setRatio(StringUtils.isNotEmpty(req.getRatio()) ? req.getRatio() : d.getRatio()); if (StringUtils.isNotEmpty(volcCallbackUrl)) { body.setCallback_url(volcCallbackUrl); } } private ByteBodyReq newVideoBody(PortalVideoGenRequest req, List content) { + String modelId = StringUtils.isNotEmpty(req.getModel()) ? req.getModel() : portalVideoProperties.resolveDefaultModelId(); + if (StringUtils.isEmpty(modelId)) { + throw new ServiceException("未配置门户视频模型,请在 application.yml 的 portal.video 中配置 models 与 defaults.model"); + } ByteBodyReq body = new ByteBodyReq(); - body.setModel(StringUtils.isNotEmpty(req.getModel()) ? req.getModel() : DEFAULT_MODEL); + body.setModel(modelId); body.setContent(content); applyOptionalParams(body, req); return body; @@ -97,6 +106,8 @@ public class PortalVideoController extends BaseController { applyOrderImages(aiOrder, req); if (req.getDuration() != null) { aiOrder.setDuration(req.getDuration()); + } else if (byteBodyReq.getDuration() != null) { + aiOrder.setDuration(byteBodyReq.getDuration()); } if (StringUtils.isNotEmpty(byteBodyReq.getResolution())) { aiOrder.setResolution(byteBodyReq.getResolution()); @@ -125,14 +136,19 @@ public class PortalVideoController extends BaseController { @PostMapping("/text-to-video") @ApiOperation("文生视频") public AjaxResult textToVideo(@RequestBody PortalVideoGenRequest request) { - if (StringUtils.isEmpty(request.getText())) { - return AjaxResult.error("请输入视频描述文本"); + List contentList; + if (request.getContent() != null && !request.getContent().isEmpty()) { + contentList = request.getContent(); + } else { + if (StringUtils.isEmpty(request.getText())) { + return AjaxResult.error("请输入视频描述文本"); + } + contentList = new ArrayList<>(); + ContentItem textItem = new ContentItem(); + textItem.setType("text"); + textItem.setText(request.getText()); + contentList.add(textItem); } - List contentList = new ArrayList<>(); - ContentItem textItem = new ContentItem(); - textItem.setType("text"); - textItem.setText(request.getText()); - contentList.add(textItem); ByteBodyReq body = newVideoBody(request, contentList); return submitOrderAndCreate(request, "text-to-video", body); @@ -218,6 +234,18 @@ public class PortalVideoController extends BaseController { return contentList; } + @GetMapping("/options") + @ApiOperation("门户视频生成可选参数(模型/比例/时长等,来自配置)") + public AjaxResult videoParamOptions() { + Map data = new LinkedHashMap<>(); + data.put("defaults", portalVideoProperties.getDefaults()); + data.put("models", portalVideoProperties.getModels()); + data.put("ratios", portalVideoProperties.getRatios()); + data.put("durations", portalVideoProperties.getDurations()); + data.put("resolutions", portalVideoProperties.getResolutions()); + return AjaxResult.success(data); + } + @GetMapping("/tasks") @ApiOperation("查询视频生成任务列表(本用户库表分页)") public TableDataInfo listMyVideoTasks(AiOrder aiOrder) { diff --git a/ruoyi-admin/src/main/java/com/ruoyi/api/request/PortalVideoGenRequest.java b/ruoyi-admin/src/main/java/com/ruoyi/api/request/PortalVideoGenRequest.java index ea4f9c6..eea6099 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/api/request/PortalVideoGenRequest.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/api/request/PortalVideoGenRequest.java @@ -1,7 +1,10 @@ package com.ruoyi.api.request; +import com.ruoyi.ai.domain.ContentItem; import lombok.Data; +import java.util.List; + /** * 门户视频生成(火山 Seedance)请求体 */ @@ -10,6 +13,11 @@ public class PortalVideoGenRequest { private String text; + /** + * 文生视频时可选:多段文本 + 参考图(与火山 content 一致);不传则仅使用 text 单行 + */ + private List content; + /** 默认与后台配置的视频计费类型一致 */ private String functionType = "21"; diff --git a/ruoyi-admin/src/main/java/com/ruoyi/config/PortalVideoProperties.java b/ruoyi-admin/src/main/java/com/ruoyi/config/PortalVideoProperties.java new file mode 100644 index 0000000..b0ea19c --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/config/PortalVideoProperties.java @@ -0,0 +1,140 @@ +package com.ruoyi.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +/** + * 门户视频生成参数(模型、比例、时长、分辨率等从配置读取,不在代码中写死业务默认值)。 + */ +@Component +@ConfigurationProperties(prefix = "portal.video") +public class PortalVideoProperties { + + private Defaults defaults = new Defaults(); + + private List models = new ArrayList<>(); + + private List ratios = new ArrayList<>(); + + private List durations = new ArrayList<>(); + + private List resolutions = new ArrayList<>(); + + public Defaults getDefaults() { + return defaults; + } + + public void setDefaults(Defaults defaults) { + if (defaults != null) { + this.defaults = defaults; + } + } + + public List getModels() { + return models; + } + + public void setModels(List models) { + this.models = models != null ? models : new ArrayList<>(); + } + + public List getRatios() { + return ratios; + } + + public void setRatios(List ratios) { + this.ratios = ratios != null ? ratios : new ArrayList<>(); + } + + public List getDurations() { + return durations; + } + + public void setDurations(List durations) { + this.durations = durations != null ? durations : new ArrayList<>(); + } + + public List getResolutions() { + return resolutions; + } + + public void setResolutions(List resolutions) { + this.resolutions = resolutions != null ? resolutions : new ArrayList<>(); + } + + /** + * defaults.model 为空时,取 models 第一项的 value。 + */ + public String resolveDefaultModelId() { + if (defaults.getModel() != null && !defaults.getModel().isEmpty()) { + return defaults.getModel(); + } + if (!models.isEmpty() && models.get(0).getValue() != null) { + return models.get(0).getValue(); + } + return null; + } + + public static class Defaults { + private String model; + private Integer duration; + private String resolution; + private String ratio; + + public String getModel() { + return model; + } + + public void setModel(String model) { + this.model = model; + } + + public Integer getDuration() { + return duration; + } + + public void setDuration(Integer duration) { + this.duration = duration; + } + + public String getResolution() { + return resolution; + } + + public void setResolution(String resolution) { + this.resolution = resolution; + } + + public String getRatio() { + return ratio; + } + + public void setRatio(String ratio) { + this.ratio = ratio; + } + } + + public static class ModelOption { + private String label; + private String value; + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } +} diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index ac03172..0d88acd 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -235,6 +235,40 @@ volcengine: apiKey: 3e33e034-7e25-4228-8864-b51b2a7a8f97 callbackUrl: https://undressing.top/api/ai/volcCallback +# 门户视频生成页:模型 / 比例 / 时长 / 分辨率均由此处维护,前后端不写死业务枚举 +portal: + video: + defaults: + model: ep-20260326165811-dlkth + duration: 4 + resolution: 720p + ratio: "3:4" + models: + - label: Seedance 2.0 + value: ep-20260326165811-dlkth + - label: Seedance 2.0 Fast + value: ep-20260326170056-dkj9m + ratios: + - "16:9" + - "9:16" + - "3:4" + - "1:1" + - "4:3" + durations: + - 4 + - 5 + - 6 + - 8 + - 10 + - 11 + - 12 + - 13 + - 14 + - 15 + resolutions: + - "720p" + - "1080p" + jinsha: url: https://api.jinshapay.xyz appId: 1763617360