fix(canvas): 视频设置 - 按渠道能力隐藏尺寸并联动分辨率宽高

This commit is contained in:
ddCat
2026-08-29 11:05:59 +08:00
parent 6d77c250c1
commit 4b75504f75
15 changed files with 196 additions and 50 deletions

View File

@@ -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,

View File

@@ -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,

View File

@@ -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<DOMRect | null>(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(" · ");

View File

@@ -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 (
<ImageSettingsTheme theme={theme}>
@@ -60,19 +57,19 @@ export function VideoSettingsPanel({ config, onConfigChange, theme, showTitle =
))}
</div>
</SettingGroup> : null}
<SettingGroup title="尺寸" color={theme.node.muted}>
<div className="grid grid-cols-[1fr_auto_1fr] items-center gap-1.5">
<DimensionInput prefix="W" value={dimensions.width} disabled={size === "auto"} theme={theme} onChange={(value) => updateDimension("width", value)} />
{sizeSupported ? <SettingGroup title="尺寸" color={theme.node.muted}>
{dimensions ? <div className="grid grid-cols-[1fr_auto_1fr] items-center gap-1.5">
<DimensionValue prefix="W" value={dimensions.width} theme={theme} />
<span className="text-xs opacity-45">×</span>
<DimensionInput prefix="H" value={dimensions.height} disabled={size === "auto"} theme={theme} onChange={(value) => updateDimension("height", value)} />
</div>
<DimensionValue prefix="H" value={dimensions.height} theme={theme} />
</div> : null}
<div className="grid grid-cols-3 gap-1.5">
{profile.ratios.map((value) => (
<button
key={value}
type="button"
className="flex h-8 cursor-pointer items-center justify-center gap-1.5 rounded-md px-1 text-[var(--fs-label)] font-medium transition-colors hover:brightness-110 focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-1"
style={{ background: normalizeRatioValue(config.size) === value ? theme.toolbar.activeBg : "transparent", color: theme.node.text, outlineColor: theme.node.muted }}
style={{ background: ratio === value ? theme.toolbar.activeBg : "transparent", color: theme.node.text, outlineColor: theme.node.muted }}
onMouseDown={(event) => event.stopPropagation()}
onClick={() => onConfigChange("size", value)}
>
@@ -81,7 +78,7 @@ export function VideoSettingsPanel({ config, onConfigChange, theme, showTitle =
</button>
))}
</div>
</SettingGroup>
</SettingGroup> : null}
<SettingGroup title="秒数" color={theme.node.muted}>
<VideoDurationControl profile={profile} value={Number(seconds)} theme={theme} disabled={(value) => !hasPriceTierForVideoSelection(priceTiers, resolution, value)} onChange={(value) => onConfigChange("videoSeconds", String(value))} />
</SettingGroup>
@@ -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 (
<label className="flex h-8 overflow-hidden rounded-md text-[var(--fs-label)]" style={{ background: theme.toolbar.itemHover, color: theme.node.text, opacity: disabled ? 0.55 : 1 }}>
<div className="flex h-8 overflow-hidden rounded-md text-[var(--fs-label)]" style={{ background: theme.toolbar.itemHover, color: theme.node.text }}>
<span className="grid w-7 place-items-center" style={{ color: theme.node.muted }}>
{prefix}
</span>
<input type="number" min={1} disabled={disabled} className="min-w-0 flex-1 bg-transparent px-2 outline-none [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none" value={value || ""} onChange={(event) => onChange(Number(event.target.value) || null)} onMouseDown={(event) => event.stopPropagation()} />
</label>
<span className="min-w-0 flex-1 px-2 leading-8 tabular-nums">{value}</span>
</div>
);
}
@@ -365,15 +362,3 @@ function SwitchRow({ label, checked, theme, onChange }: { label: string; checked
</div>
);
}
function readSizeDimensions(size: string) {
if (size === "auto") return { width: 0, height: 0 };
const match = size.match(/^(\d+)x(\d+)$/);
return { width: Number(match?.[1]) || 1280, height: Number(match?.[2]) || 720 };
}
function normalizeRatioValue(value: string) {
const match = String(value || "").match(/^(\d+(?:\.\d+)?)x(\d+(?:\.\d+)?)$/);
if (!match) return value;
return `${match[1]}:${match[2]}`;
}