From 4b75504f752f95a54908bdf3d15e6f2c05a4f47d Mon Sep 17 00:00:00 2001 From: ddCat <2695092045@qq.com> Date: Sat, 29 Aug 2026 11:05:59 +0800 Subject: [PATCH] =?UTF-8?q?fix(canvas):=20=E8=A7=86=E9=A2=91=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=20-=20=E6=8C=89=E6=B8=A0=E9=81=93=E8=83=BD=E5=8A=9B?= =?UTF-8?q?=E9=9A=90=E8=97=8F=E5=B0=BA=E5=AF=B8=E5=B9=B6=E8=81=94=E5=8A=A8?= =?UTF-8?q?=E5=88=86=E8=BE=A8=E7=8E=87=E5=AE=BD=E9=AB=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/service/model_capability.go | 8 ++- .../internal/service/model_capability_test.go | 19 +++++++ docs/content/docs/progress/pending-test.mdx | 5 ++ .../canvas/canvas-config-node-panel.tsx | 4 +- .../canvas/canvas-node-prompt-panel.tsx | 4 +- .../canvas/canvas-video-settings-popover.tsx | 6 ++- web/src/components/video-settings-panel.tsx | 47 ++++++----------- .../lib/canvas/canvas-project-generation.ts | 4 +- web/src/lib/model-capabilities.ts | 24 ++++++++- web/src/lib/video-generation-options.ts | 26 ++++++++++ web/src/pages/create/index.tsx | 7 +-- .../pages/settings/channel-settings-pane.tsx | 4 +- web/test/canvas-model-policy.test.ts | 25 +++++++++ web/test/channel-model-catalog.test.ts | 52 ++++++++++++++++++- web/test/video-generation-options.test.ts | 11 +++- 15 files changed, 196 insertions(+), 50 deletions(-) diff --git a/backend/internal/service/model_capability.go b/backend/internal/service/model_capability.go index 909afab4a..fcb8bf2c1 100644 --- a/backend/internal/service/model_capability.go +++ b/backend/internal/service/model_capability.go @@ -532,8 +532,12 @@ func validateVideoCapabilityConfig(value *VideoCapabilityConfig) error { if err := validateVideoDuration(value.Duration); err != nil { return err } - if len(value.Ratios) == 0 || strings.TrimSpace(value.DefaultRatio) == "" || !containsCapabilityString(value.Ratios, value.DefaultRatio) { - return BadAuthRequest("请至少配置一个画面比例,并选择默认比例") + if len(value.Ratios) == 0 { + if strings.TrimSpace(value.DefaultRatio) != "" { + return BadAuthRequest("未配置画面比例时不能设置默认比例") + } + } else if strings.TrimSpace(value.DefaultRatio) == "" || !containsCapabilityString(value.Ratios, value.DefaultRatio) { + return BadAuthRequest("默认画面比例必须属于支持值") } if len(value.Resolutions) == 0 { if strings.TrimSpace(value.DefaultResolution) != "" { diff --git a/backend/internal/service/model_capability_test.go b/backend/internal/service/model_capability_test.go index 40573db30..5ce98f587 100644 --- a/backend/internal/service/model_capability_test.go +++ b/backend/internal/service/model_capability_test.go @@ -237,6 +237,25 @@ func TestNormalizeVideoCapabilityAllowsOmittedResolution(t *testing.T) { } } +func TestNormalizeVideoCapabilityAllowsOmittedRatio(t *testing.T) { + profile := DefaultModelCapabilityConfigForModel("autodl-comfyui", "minimax_h3_lightx2v_no_pic").Video + profile.Ratios = nil + profile.DefaultRatio = "" + profile.Resolutions = []string{"480p竖", "480p横"} + profile.DefaultResolution = "480p竖" + + result, err := NormalizeModelCapabilityConfig("video", "autodl-comfyui", &ModelCapabilityConfig{Version: 1, Video: profile}) + if err != nil { + t.Fatalf("NormalizeModelCapabilityConfig() error = %v", err) + } + if result.Video == nil || len(result.Video.Ratios) != 0 || result.Video.DefaultRatio != "" { + t.Fatalf("normalized video ratios = %#v", result.Video) + } + if err := validateVideoTask(result.Video, canvasGenerationInput{Config: providerConfig{Model: "minimax_h3_lightx2v_no_pic", VideoSeconds: "6", VQuality: "480p竖"}}); err != nil { + t.Fatalf("validateVideoTask() error = %v", err) + } +} + func TestCapabilitySpecFromModelCapabilityConfigProjectsImageSizeOnce(t *testing.T) { config := &ModelCapabilityConfig{ Version: 1, diff --git a/docs/content/docs/progress/pending-test.mdx b/docs/content/docs/progress/pending-test.mdx index 4d45ca22d..c1aa402ad 100644 --- a/docs/content/docs/progress/pending-test.mdx +++ b/docs/content/docs/progress/pending-test.mdx @@ -375,9 +375,14 @@ description: 影策已实现功能的手工验证清单与未确认边界 - [x] 前端分辨率规范化、画布选中态、价格匹配与请求映射专项测试通过。 - [x] AutoDL 插件 `1.0.2` 已改用通用 `lower` 并通过协议包测试;宿主仅保留旧 `1.0.1` 清单所需的兼容 shim。 +- [x] Manifest 工作流的视频能力改为按 `parameters` 投影:只声明分辨率时不再补画幅能力,也不会继续提交旧的全局 `size`。 +- [x] 同时支持分辨率和画幅的渠道按当前比例与分辨率推导只读宽高,例如 `16:9 + 720P = 1280×720`、`16:9 + 1080P = 1920×1080`、`16:9 + 4K = 3840×2160`。 - [x] `web` TypeScript 检查与生产构建通过。 +- [x] 后端允许视频能力明确省略画幅,并通过全量 Go 测试;前端能力投影、尺寸计算和画布模型专项测试通过。 - [ ] 在画布选择 `480p竖`、`768p竖`、`480p横`、`768p横`,确认按钮选中态、底部摘要和重新打开后的状态一致。 - [ ] 分别提交四个枚举值,确认请求中的 `resolution` 与插件声明一致,且其他标准 `720p`、`1080p` 渠道不受影响。 +- [ ] 使用仅声明 `resolution` 的 AutoDL 工作流,确认画布和创作页不显示画幅/尺寸,生成请求不包含无意义的 `aspectRatio`。 +- [ ] 使用同时声明画幅与分辨率的渠道,依次切换 `16:9` 下的 720P、1080P、4K 及 `9:16`,确认宽高展示随选择即时更新且请求仍按渠道协议发送比例与分辨率。 ## 画布生成重复提交保护 diff --git a/web/src/components/canvas/canvas-config-node-panel.tsx b/web/src/components/canvas/canvas-config-node-panel.tsx index 032fcbf14..4b1e35c1c 100644 --- a/web/src/components/canvas/canvas-config-node-panel.tsx +++ b/web/src/components/canvas/canvas-config-node-panel.tsx @@ -585,10 +585,10 @@ function buildModelNodeConfig(globalConfig: AiConfig, node: CanvasNodeData, mode taskWorkflowProvider: "model", model, quality: generationDefaults.quality || globalConfig.quality || defaultConfig.quality, - size: generationDefaults.size || globalConfig.size || defaultConfig.size, + size: generationDefaults.size ?? globalConfig.size ?? defaultConfig.size, transparentBackground: generationDefaults.transparentBackground || "false", videoSeconds: generationDefaults.videoSeconds || normalizeVideoDuration(globalConfig.videoSeconds || defaultConfig.videoSeconds), - vquality: generationDefaults.vquality || normalizeVideoResolution(globalConfig.vquality || defaultConfig.vquality), + vquality: generationDefaults.vquality ?? normalizeVideoResolution(globalConfig.vquality || defaultConfig.vquality), videoGenerateAudio: videoProfile?.generateAudio.supported ? generationDefaults.videoGenerateAudio || String(videoProfile.generateAudio.default) : "false", videoWatermark: videoProfile?.watermark.supported ? generationDefaults.videoWatermark || String(videoProfile.watermark.default) : "false", audioVoice: node.metadata?.audioVoice || globalConfig.audioVoice || defaultConfig.audioVoice, diff --git a/web/src/components/canvas/canvas-node-prompt-panel.tsx b/web/src/components/canvas/canvas-node-prompt-panel.tsx index 06e31930b..1e095877b 100644 --- a/web/src/components/canvas/canvas-node-prompt-panel.tsx +++ b/web/src/components/canvas/canvas-node-prompt-panel.tsx @@ -738,10 +738,10 @@ function buildNodeConfig(globalConfig: AiConfig, node: CanvasNodeData, mode: Can ...globalConfig, model, quality: defaults.quality || globalConfig.quality || defaultConfig.quality, - size: defaults.size || globalConfig.size || defaultConfig.size, + size: defaults.size ?? globalConfig.size ?? defaultConfig.size, transparentBackground: defaults.transparentBackground || "false", videoSeconds: defaults.videoSeconds || normalizeVideoDuration(globalConfig.videoSeconds || defaultConfig.videoSeconds), - vquality: defaults.vquality || normalizeVideoResolution(globalConfig.vquality || defaultConfig.vquality), + vquality: defaults.vquality ?? normalizeVideoResolution(globalConfig.vquality || defaultConfig.vquality), videoGenerateAudio: defaults.videoGenerateAudio || globalConfig.videoGenerateAudio || defaultConfig.videoGenerateAudio, videoWatermark: defaults.videoWatermark || globalConfig.videoWatermark || defaultConfig.videoWatermark, audioVoice: node.metadata?.audioVoice || globalConfig.audioVoice || defaultConfig.audioVoice, diff --git a/web/src/components/canvas/canvas-video-settings-popover.tsx b/web/src/components/canvas/canvas-video-settings-popover.tsx index 9fff43ea3..f013fd7c3 100644 --- a/web/src/components/canvas/canvas-video-settings-popover.tsx +++ b/web/src/components/canvas/canvas-video-settings-popover.tsx @@ -5,7 +5,7 @@ import { Button } from "antd"; import { VideoSettingsPanel, videoResolutionLabel, videoSecondsLabel, videoSizeLabel } from "@/components/video-settings-panel"; import { canvasThemes } from "@/lib/canvas-theme"; -import { modelCapabilityConfigFor, resolveVideoResolutionValue } from "@/lib/model-capabilities"; +import { modelCapabilityConfigFor, resolveVideoRatioValue, resolveVideoResolutionValue } from "@/lib/model-capabilities"; import { useThemeStore } from "@/stores/use-theme-store"; import type { AiConfig } from "@/stores/use-config-store"; @@ -24,10 +24,12 @@ export function CanvasVideoSettingsPopover({ config, onConfigChange, buttonClass const [buttonRect, setButtonRect] = useState(null); const videoProfile = modelCapabilityConfigFor(config, config.model).video; const resolutionSupported = Boolean(videoProfile?.resolutions.length); + const sizeSupported = Boolean(videoProfile?.ratios.length); const resolution = videoProfile ? resolveVideoResolutionValue(videoProfile, config.vquality) : ""; + const size = videoProfile ? resolveVideoRatioValue(videoProfile, config.size) : ""; const summary = [ ...(resolutionSupported ? [videoResolutionLabel(resolution)] : []), - videoSizeLabel(config.size), + ...(sizeSupported ? [videoSizeLabel(size)] : []), videoSecondsLabel(config.videoSeconds), ].join(" · "); diff --git a/web/src/components/video-settings-panel.tsx b/web/src/components/video-settings-panel.tsx index 44572bff5..9add1434d 100644 --- a/web/src/components/video-settings-panel.tsx +++ b/web/src/components/video-settings-panel.tsx @@ -4,8 +4,8 @@ import { Switch } from "antd"; import { ImageSettingsTheme } from "@/components/image-settings-panel"; import { boolConfig, isSeedanceFastModel, isSeedanceVideoConfig, normalizeSeedanceDuration, normalizeSeedanceRatio, normalizeSeedanceResolution, seedanceRatioOptions } from "@/lib/seedance-video"; import { type CanvasTheme } from "@/lib/canvas-theme"; -import { formatVideoResolutionLabel, normalizeVideoDuration, videoResolutionComparisonKey, VIDEO_DURATION_MIN } from "@/lib/video-generation-options"; -import { modelCapabilityConfigFor, resolveVideoResolutionValue, videoDurationOptions, type VideoCapabilityConfig } from "@/lib/model-capabilities"; +import { formatVideoResolutionLabel, normalizeVideoDuration, videoDimensionsForRatioAndResolution, videoResolutionComparisonKey, VIDEO_DURATION_MIN } from "@/lib/video-generation-options"; +import { modelCapabilityConfigFor, resolveVideoRatioValue, resolveVideoResolutionValue, videoDurationOptions, type VideoCapabilityConfig } from "@/lib/model-capabilities"; import { modelOptionName, resolveModelChannel, resolveModelRequestConfig, type AiConfig } from "@/stores/use-config-store"; const sizeOptions = [ @@ -36,16 +36,13 @@ export function VideoSettingsPanel({ config, onConfigChange, theme, showTitle = } const seconds = normalizeVideoDuration(config.videoSeconds); - const size = normalizeVideoSizeValue(config.size); - const dimensions = readSizeDimensions(size); const resolution = resolveVideoResolutionValue(profile, config.vquality); + const ratio = resolveVideoRatioValue(profile, config.size); + const dimensions = videoDimensionsForRatioAndResolution(ratio, resolution); + const sizeSupported = profile.ratios.length > 0; const configuredResolutions = profile.resolutions.map((value) => ({ value, label: formatVideoResolutionLabel(value) })); const generateAudio = boolConfig(config.videoGenerateAudio, profile.generateAudio.default); const watermark = boolConfig(config.videoWatermark, profile.watermark.default); - const updateDimension = (key: "width" | "height", value: number | null) => { - const next = Math.max(1, Math.floor(value || dimensions[key] || 720)); - onConfigChange("size", `${key === "width" ? next : dimensions.width}x${key === "height" ? next : dimensions.height}`); - }; return ( @@ -60,19 +57,19 @@ export function VideoSettingsPanel({ config, onConfigChange, theme, showTitle = ))} : null} - -
- updateDimension("width", value)} /> + {sizeSupported ? + {dimensions ?
+ × - updateDimension("height", value)} /> -
+ +
: null}
{profile.ratios.map((value) => ( ))}
-
+ : null} !hasPriceTierForVideoSelection(priceTiers, resolution, value)} onChange={(value) => onConfigChange("videoSeconds", String(value))} /> @@ -224,14 +221,14 @@ function SettingGroup({ title, color, children }: { title: string; color: string ); } -function DimensionInput({ prefix, value, disabled, theme, onChange }: { prefix: string; value: number; disabled: boolean; theme: CanvasTheme; onChange: (value: number | null) => void }) { +function DimensionValue({ prefix, value, theme }: { prefix: string; value: number; theme: CanvasTheme }) { return ( -