fix(canvas): 视频设置 - 按渠道能力隐藏尺寸并联动分辨率宽高
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(" · ");
|
||||
|
||||
|
||||
@@ -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]}`;
|
||||
}
|
||||
|
||||
@@ -498,10 +498,10 @@ export function buildGenerationConfig(config: AiConfig, node: CanvasNodeData | u
|
||||
comfyBridge,
|
||||
model,
|
||||
quality: generationDefaults.quality || requestedConfig.quality,
|
||||
size: generationDefaults.size || requestedConfig.size,
|
||||
size: generationDefaults.size ?? requestedConfig.size,
|
||||
transparentBackground: generationDefaults.transparentBackground || (requestedConfig.transparentBackground === "true" ? "true" : "false"),
|
||||
videoSeconds: generationDefaults.videoSeconds || requestedConfig.videoSeconds,
|
||||
vquality: generationDefaults.vquality || requestedConfig.vquality,
|
||||
vquality: generationDefaults.vquality ?? requestedConfig.vquality,
|
||||
videoGenerateAudio: generationDefaults.videoGenerateAudio || requestedConfig.videoGenerateAudio,
|
||||
videoWatermark: generationDefaults.videoWatermark || requestedConfig.videoWatermark,
|
||||
videoArkPrivateAssetUpload: requestedConfig.videoArkPrivateAssetUpload,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ModelProtocol } from "@/lib/model-protocols";
|
||||
import type { ModelProtocol, ModelProtocolWorkflow } from "@/lib/model-protocols";
|
||||
|
||||
export type ModelCapabilityConfig = {
|
||||
version: number;
|
||||
@@ -335,6 +335,22 @@ export function defaultModelCapabilityConfig(protocol?: ModelProtocol, model = "
|
||||
return { version: 1, text, image: defaultImageCapabilityConfig(protocol, model), video };
|
||||
}
|
||||
|
||||
export function pluginWorkflowCapabilityConfig(protocol: ModelProtocol, workflow: ModelProtocolWorkflow): ModelCapabilityConfig | undefined {
|
||||
if (workflow.capability !== "image" && workflow.capability !== "video") return undefined;
|
||||
const fallback = defaultModelCapabilityConfig(protocol, workflow.id);
|
||||
const fields: WorkflowVideoFieldLike[] = workflow.parameters.map((parameter) => ({
|
||||
fieldName: parameter.name,
|
||||
source: parameter.mapping,
|
||||
fieldType: parameter.type,
|
||||
options: parameter.values,
|
||||
defaultValue: workflow.defaults?.[parameter.name],
|
||||
}));
|
||||
if (workflow.capability === "image") {
|
||||
return { ...fallback, image: workflowImageCapabilityConfig(fields, fallback.image!) };
|
||||
}
|
||||
return { ...fallback, video: workflowVideoCapabilityConfig(fields, fallback.video!) };
|
||||
}
|
||||
|
||||
export function modelCapabilityConfigFor(config: { channels: Array<{ id: string; models: string[]; modelCosts?: Array<{ model: string; capabilityConfig?: ModelCapabilityConfig; protocol?: ModelProtocol }> }> }, model: string) {
|
||||
const separator = model.indexOf("::");
|
||||
const channelId = separator >= 0 ? model.slice(0, separator) : "";
|
||||
@@ -900,12 +916,16 @@ export function imageSizeRequest(profile: ImageCapabilityConfig, value?: string)
|
||||
|
||||
export function normalizeVideoValue(profile: VideoCapabilityConfig, value: { seconds?: string; ratio?: string; resolution?: string }) {
|
||||
const duration = profile.duration.selection === "enum" ? ((profile.duration.values || []).includes(Number(value.seconds)) ? Number(value.seconds) : profile.duration.default) : normalizeRangeDuration(profile, Number(value.seconds));
|
||||
const ratio = profile.ratios.includes(value.ratio || "") ? value.ratio! : profile.defaultRatio;
|
||||
const ratio = resolveVideoRatioValue(profile, value.ratio);
|
||||
// 前端状态历史上保存过 `720`,而能力配置和供应商通常使用 `720p`;统一按能力中的原始值返回,避免被误判为不支持。
|
||||
const resolution = resolveVideoResolutionValue(profile, value.resolution);
|
||||
return { seconds: String(duration), ratio, resolution };
|
||||
}
|
||||
|
||||
export function resolveVideoRatioValue(profile: VideoCapabilityConfig, value: string | undefined) {
|
||||
return profile.ratios.includes(value || "") ? value! : profile.defaultRatio || profile.ratios[0] || "";
|
||||
}
|
||||
|
||||
export function resolveVideoResolutionValue(profile: VideoCapabilityConfig, value: string | undefined) {
|
||||
return videoResolutionRequest(profile, value) || profile.defaultResolution || profile.resolutions[0] || "";
|
||||
}
|
||||
|
||||
@@ -38,3 +38,29 @@ export function formatVideoResolutionLabel(value: string | number | undefined) {
|
||||
if (/^\d+$/.test(normalized)) return `${normalized}P`;
|
||||
return normalized.replace(/^(\d+)p/i, "$1P");
|
||||
}
|
||||
|
||||
export function videoDimensionsForRatioAndResolution(ratio: string | undefined, resolution: string | number | undefined) {
|
||||
const ratioMatch = String(ratio || "")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace("×", "x")
|
||||
.match(/^(\d+(?:\.\d+)?)(?::|x)(\d+(?:\.\d+)?)$/);
|
||||
if (!ratioMatch) return undefined;
|
||||
|
||||
const ratioWidth = Number(ratioMatch[1]);
|
||||
const ratioHeight = Number(ratioMatch[2]);
|
||||
if (!Number.isFinite(ratioWidth) || !Number.isFinite(ratioHeight) || ratioWidth <= 0 || ratioHeight <= 0) return undefined;
|
||||
|
||||
const normalizedResolution = normalizeVideoResolution(resolution);
|
||||
const resolutionMatch = normalizedResolution.match(/^(\d+)(?:p)?/i);
|
||||
const shortEdge = Number(resolutionMatch?.[1]);
|
||||
if (!Number.isFinite(shortEdge) || shortEdge <= 0) return undefined;
|
||||
|
||||
const aspect = ratioWidth / ratioHeight;
|
||||
if (aspect >= 1) return { width: evenDimension(shortEdge * aspect), height: shortEdge };
|
||||
return { width: shortEdge, height: evenDimension(shortEdge / aspect) };
|
||||
}
|
||||
|
||||
function evenDimension(value: number) {
|
||||
return Math.max(2, Math.round(value / 2) * 2);
|
||||
}
|
||||
|
||||
@@ -612,7 +612,7 @@ export default function CreatePage() {
|
||||
...(mode === "image"
|
||||
? { size: normalizedImage?.size || ratio, quality: normalizedImage?.quality || quality, count: normalizedImage?.count || count, videoSeconds: config.videoSeconds }
|
||||
: mode === "video"
|
||||
? { size: normalizedVideo?.ratio || ratio, videoSeconds: normalizedVideo?.seconds || seconds, vquality: (normalizedVideo?.resolution || videoQuality).replace(/p$/i, "") }
|
||||
? { size: normalizedVideo?.ratio ?? ratio, videoSeconds: normalizedVideo?.seconds || seconds, vquality: (normalizedVideo?.resolution ?? videoQuality).replace(/p$/i, "") }
|
||||
: {}),
|
||||
};
|
||||
try {
|
||||
@@ -1519,9 +1519,10 @@ function GenerationSettingsMenu(props: ComposerProps) {
|
||||
...(props.imageProfile.quality.supported ? [qualityLabel] : []),
|
||||
...(props.imageProfile.maxOutputs > 1 ? [props.count] : []),
|
||||
].join(" · ");
|
||||
const summary = props.mode === "video" ? [props.ratio, ...(videoResolutionSupported ? [videoResolutionLabel(props.videoQuality)] : [])].join(" · ") : imageSummary;
|
||||
const videoRatioSupported = props.mode === "video" && ratios.length > 0;
|
||||
const summary = props.mode === "video" ? [...(videoRatioSupported ? [props.ratio] : []), ...(videoResolutionSupported ? [videoResolutionLabel(props.videoQuality)] : [])].join(" · ") : imageSummary;
|
||||
const panel = <div className="creation-parameter-menu">
|
||||
{props.mode === "video" || mergedProfile.size.parameter !== "none" ? <SettingSection title="画幅" value={referenceImageSizeSelected ? referenceImageSizeLabel : props.mode === "image" && usesImageResolutionPicker ? activeImageRatio : props.ratio}><div className="creation-parameter-content"><div className="creation-choice-grid is-ratio">{referenceImageSizeValue ? <button type="button" aria-pressed={referenceImageSizeSelected} aria-label={"使用参考图尺寸 " + referenceImageSizeLabel} title={"使用参考图尺寸 " + referenceImageSizeLabel} className={"creation-reference-size-choice" + (referenceImageSizeSelected ? " is-selected" : "")} onClick={selectReferenceImageSize}><span className="creation-ratio-preview"><span style={ratioPreviewStyle(referenceImageSizeRatio)} /></span><span>参考图</span></button> : null}{ratios.map((value) => { const selected = props.mode === "image" && usesImageResolutionPicker ? value === activeImageRatio : value === props.ratio; return <button key={value} type="button" aria-pressed={selected} className={selected ? "is-selected" : ""} onClick={() => { if (props.mode === "image") selectImageRatio(value); else props.setRatio(value); setCustomRatioOpen(false); }}><span className="creation-ratio-preview"><span style={ratioPreviewStyle(value)} /></span><span>{value}</span></button>; })}</div>{props.mode !== "video" && mergedProfile.size.allowCustom && (customRatioOpen ? <label className="creation-custom-value"><span>宽 x 高</span><input value={props.ratio} onFocus={(event) => event.currentTarget.select()} onChange={(event) => props.setRatio(event.target.value)} placeholder="1920x1080 或 2:1" aria-label="自定义图片尺寸或比例" /></label> : <button type="button" className="creation-custom-trigger" onClick={() => setCustomRatioOpen(true)}><Plus />输入自定义尺寸</button>)}</div></SettingSection> : null}
|
||||
{videoRatioSupported || props.mode !== "video" && mergedProfile.size.parameter !== "none" ? <SettingSection title="画幅" value={referenceImageSizeSelected ? referenceImageSizeLabel : props.mode === "image" && usesImageResolutionPicker ? activeImageRatio : props.ratio}><div className="creation-parameter-content"><div className="creation-choice-grid is-ratio">{referenceImageSizeValue ? <button type="button" aria-pressed={referenceImageSizeSelected} aria-label={"使用参考图尺寸 " + referenceImageSizeLabel} title={"使用参考图尺寸 " + referenceImageSizeLabel} className={"creation-reference-size-choice" + (referenceImageSizeSelected ? " is-selected" : "")} onClick={selectReferenceImageSize}><span className="creation-ratio-preview"><span style={ratioPreviewStyle(referenceImageSizeRatio)} /></span><span>参考图</span></button> : null}{ratios.map((value) => { const selected = props.mode === "image" && usesImageResolutionPicker ? value === activeImageRatio : value === props.ratio; return <button key={value} type="button" aria-pressed={selected} className={selected ? "is-selected" : ""} onClick={() => { if (props.mode === "image") selectImageRatio(value); else props.setRatio(value); setCustomRatioOpen(false); }}><span className="creation-ratio-preview"><span style={ratioPreviewStyle(value)} /></span><span>{value}</span></button>; })}</div>{props.mode !== "video" && mergedProfile.size.allowCustom && (customRatioOpen ? <label className="creation-custom-value"><span>宽 x 高</span><input value={props.ratio} onFocus={(event) => event.currentTarget.select()} onChange={(event) => props.setRatio(event.target.value)} placeholder="1920x1080 或 2:1" aria-label="自定义图片尺寸或比例" /></label> : <button type="button" className="creation-custom-trigger" onClick={() => setCustomRatioOpen(true)}><Plus />输入自定义尺寸</button>)}</div></SettingSection> : null}
|
||||
{props.mode === "video" ? (videoResolutionSupported ? <SettingSection title="清晰度" value={videoResolutionLabel(props.videoQuality)}><div className="creation-choice-grid is-resolution">{resolutions.map((option) => <button key={option.value} type="button" aria-pressed={option.value === props.videoQuality} className={option.value === props.videoQuality ? "is-selected" : ""} onClick={() => props.setVideoQuality(option.value)}>{option.label}</button>)}</div></SettingSection> : null) : <>
|
||||
{imageResolutionChoiceOptions.length ? <SettingSection title="分辨率" value={activeImageResolutionChoice === "auto" ? "自动" : activeImageResolutionChoice.toUpperCase()}><div className="creation-choice-grid is-resolution">{imageResolutionChoiceOptions.map((choice) => <button key={choice} type="button" aria-pressed={choice === activeImageResolutionChoice} className={choice === activeImageResolutionChoice ? "is-selected" : ""} onClick={() => selectImageResolution(choice)}>{choice === "auto" ? "自动" : choice.toUpperCase()}</button>)}</div></SettingSection> : null}
|
||||
{props.imageProfile.quality.supported ? <SettingSection title={activeQualityOptions.some((item) => item.value === "1k" || item.value === "2k") ? "分辨率" : "图片质量"} value={qualityLabel}><div className="creation-choice-grid is-quality">{activeQualityOptions.map((option) => <button key={option.value} type="button" aria-pressed={option.value === props.quality} className={option.value === props.quality ? "is-selected" : ""} onClick={() => props.setQuality(option.value)}><span>{option.label}</span><small>{option.description}</small></button>)}</div></SettingSection> : null}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { mergeFetchedChannelModelCosts } from "@/lib/channel-model-catalog";
|
||||
import { desktopLocalChannelFormState, desktopLocalChannelPayloadValue, DESKTOP_LOCAL_CHANNEL_EXAMPLE_BASE_URL } from "@/lib/desktop-local-channel";
|
||||
import { fetchChannelModels } from "@/services/api/image";
|
||||
import { fetchPluginProviderCatalog } from "@/services/api/plugin-catalog";
|
||||
import { defaultModelCapabilityConfig } from "@/lib/model-capabilities";
|
||||
import { pluginWorkflowCapabilityConfig } from "@/lib/model-capabilities";
|
||||
import type { ModelProtocolDefinition } from "@/lib/model-protocols";
|
||||
import {
|
||||
createModelChannel,
|
||||
@@ -84,7 +84,7 @@ export function ChannelSettingsPane({ onOpenModels, onOpenRunningHub, onOpenComf
|
||||
protocol: provider.value,
|
||||
billingMode: "fixed_request" as const,
|
||||
unitPriceMicrocredits: 0,
|
||||
capabilityConfig: workflow.capability === "image" || workflow.capability === "video" ? defaultModelCapabilityConfig(provider.value, workflow.id) : undefined,
|
||||
capabilityConfig: pluginWorkflowCapabilityConfig(provider.value, workflow),
|
||||
defaultOptions: workflow.defaults,
|
||||
}))
|
||||
: channel.modelCosts;
|
||||
|
||||
Reference in New Issue
Block a user