* feat(image): 图片参数 - 统一插件声明配置与生成合同 * docs(image): 图片参数 - 说明声明合同与验证边界 * fix(plugins): 图片声明 - 保留内联素材支持并同步制品文档 * fix(image): 参数合同 - 对齐审批意图和尺寸预检调用方 * fix(image): 调用链 - 保留公开意图并接通独立分辨率 * fix(image): 图片参数 - 补齐分辨率接线与目录价格投影
521 lines
20 KiB
Go
521 lines
20 KiB
Go
package app
|
||
|
||
import (
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"strings"
|
||
|
||
"yingce/backend/internal/model"
|
||
protocolcontract "yingce/backend/internal/protocol"
|
||
)
|
||
|
||
// ModelCapabilityConfig 是模型能力声明,不包含供应商字段名;协议适配器负责把统一参数映射到上游请求。
|
||
type ModelCapabilityConfig struct {
|
||
Version int `json:"version"`
|
||
Text *TextCapabilityConfig `json:"text,omitempty"`
|
||
Image *ImageCapabilityConfig `json:"image,omitempty"`
|
||
Video *VideoCapabilityConfig `json:"video,omitempty"`
|
||
}
|
||
|
||
type TextCapabilityConfig struct {
|
||
// Streaming controls whether this model accepts upstream SSE text responses.
|
||
// A nil value is treated as true for backwards compatibility with older configs.
|
||
Streaming *bool `json:"streaming,omitempty"`
|
||
// ContextWindowTokens is the provider's total input plus output context window.
|
||
// It is a model contract, not an application transport ceiling.
|
||
ContextWindowTokens int `json:"contextWindowTokens"`
|
||
// MaxOutputTokens is the provider's maximum completion/reasoning budget.
|
||
MaxOutputTokens int `json:"maxOutputTokens"`
|
||
References TextReferenceConfig `json:"references"`
|
||
}
|
||
|
||
type TextReferenceConfig struct {
|
||
PromptMaxChars int `json:"promptMaxChars"`
|
||
MaxImages int `json:"maxImages"`
|
||
MaxImageBytes int64 `json:"maxImageBytes"`
|
||
MaxVideos int `json:"maxVideos"`
|
||
MaxVideoBytes int64 `json:"maxVideoBytes"`
|
||
}
|
||
|
||
type ImageCapabilityConfig struct {
|
||
Parameters []protocolcontract.ImageParameter `json:"parameters"`
|
||
References ImageReferenceConfig `json:"references"`
|
||
Size ImageSizeConfig `json:"size"`
|
||
Resolution ImageResolutionConfig `json:"resolution"`
|
||
Quality ImageQualityConfig `json:"quality"`
|
||
// ResolutionUsesQuality is reserved for protocols whose documented quality
|
||
// values are actually resolution tiers (for example Gemini imageSize).
|
||
ResolutionUsesQuality bool `json:"resolutionUsesQuality,omitempty"`
|
||
TransparentBackground VideoBooleanConfig `json:"transparentBackground"`
|
||
ResponseFormat ParameterSupport `json:"responseFormat"`
|
||
OutputFormat ParameterSupport `json:"outputFormat"`
|
||
MaxOutputs int `json:"maxOutputs"`
|
||
}
|
||
|
||
type ImageReferenceConfig struct {
|
||
PromptMaxChars int `json:"promptMaxChars"`
|
||
MaxImages int `json:"maxImages"`
|
||
MaxImageBytes int64 `json:"maxImageBytes"`
|
||
MaskSupported bool `json:"maskSupported"`
|
||
}
|
||
|
||
type ImageSizeConfig struct {
|
||
Parameter string `json:"parameter"`
|
||
Values []string `json:"values"`
|
||
Default string `json:"default"`
|
||
AllowCustom bool `json:"allowCustom"`
|
||
Presets []ImageSizePreset `json:"presets,omitempty"`
|
||
DefaultTier string `json:"defaultTier,omitempty"`
|
||
}
|
||
|
||
type ImageSizePreset struct {
|
||
Tier string `json:"tier"`
|
||
Ratio string `json:"ratio"`
|
||
Size string `json:"size"`
|
||
Width int `json:"width"`
|
||
Height int `json:"height"`
|
||
}
|
||
|
||
type ImageQualityConfig struct {
|
||
Supported bool `json:"supported"`
|
||
Values []string `json:"values"`
|
||
Default string `json:"default"`
|
||
}
|
||
|
||
type ImageResolutionConfig struct {
|
||
Supported bool `json:"supported"`
|
||
Values []string `json:"values"`
|
||
Default string `json:"default"`
|
||
}
|
||
|
||
type ParameterSupport struct {
|
||
Supported bool `json:"supported"`
|
||
}
|
||
|
||
type VideoCapabilityConfig struct {
|
||
References VideoReferenceConfig `json:"references"`
|
||
Duration VideoDurationConfig `json:"duration"`
|
||
DurationSupported *bool `json:"durationSupported,omitempty"`
|
||
Ratios []string `json:"ratios"`
|
||
DefaultRatio string `json:"defaultRatio"`
|
||
Resolutions []string `json:"resolutions"`
|
||
DefaultResolution string `json:"defaultResolution"`
|
||
FixedScreenSpec *VideoScreenSpecConfig `json:"fixedScreenSpec,omitempty"`
|
||
GenerateAudio VideoBooleanConfig `json:"generateAudio"`
|
||
Watermark VideoBooleanConfig `json:"watermark"`
|
||
Operations []string `json:"operations"`
|
||
DefaultOperation string `json:"defaultOperation"`
|
||
}
|
||
|
||
type VideoScreenSpecConfig struct {
|
||
Ratios []string `json:"ratios"`
|
||
DefaultRatio string `json:"defaultRatio"`
|
||
Resolutions []string `json:"resolutions"`
|
||
DefaultResolution string `json:"defaultResolution"`
|
||
}
|
||
|
||
type VideoReferenceConfig struct {
|
||
PromptMaxChars int `json:"promptMaxChars"`
|
||
MinImages int `json:"minImages"`
|
||
MaxImages int `json:"maxImages"`
|
||
MaxImageBytes int64 `json:"maxImageBytes"`
|
||
MaxVideos int `json:"maxVideos"`
|
||
MaxVideoBytes int64 `json:"maxVideoBytes"`
|
||
MaxVideoDuration int `json:"maxVideoDurationSeconds"`
|
||
MaxAudios int `json:"maxAudios"`
|
||
MaxAudioBytes int64 `json:"maxAudioBytes"`
|
||
MaxAudioDuration int `json:"maxAudioDurationSeconds"`
|
||
}
|
||
|
||
// DefaultVideoPromptMaxChars 是普通视频模型提示词字符数的默认上限。
|
||
// 视频提示词由输入框文本、连线内容和技能上下文合成,远长于用户手输内容;
|
||
// 默认值过小会把画布工作流正常可用的提示词拦在本地。管理员仍可按模型覆盖。
|
||
const DefaultVideoPromptMaxChars = 8000
|
||
|
||
type VideoDurationConfig struct {
|
||
Selection string `json:"selection"`
|
||
Min int `json:"min,omitempty"`
|
||
Max int `json:"max,omitempty"`
|
||
Step int `json:"step,omitempty"`
|
||
Values []int `json:"values,omitempty"`
|
||
Default int `json:"default"`
|
||
}
|
||
|
||
type VideoBooleanConfig struct {
|
||
Supported bool `json:"supported"`
|
||
Default bool `json:"default"`
|
||
}
|
||
|
||
func DecodeModelCapabilityConfig(raw string) (*ModelCapabilityConfig, error) {
|
||
if strings.TrimSpace(raw) == "" {
|
||
return nil, nil
|
||
}
|
||
var value ModelCapabilityConfig
|
||
if err := json.Unmarshal([]byte(raw), &value); err != nil {
|
||
return nil, err
|
||
}
|
||
return &value, nil
|
||
}
|
||
|
||
// normalizedChannelModelCapability 从持久化记录恢复渠道模型的权威能力合同。
|
||
// 目录读取可以选择隔离损坏记录;任务创建等写路径必须把错误向上返回并失败关闭。
|
||
func normalizedChannelModelCapability(channelModel *model.ChannelModel, registries ...*protocolcontract.Registry) (*ModelCapabilityConfig, error) {
|
||
if channelModel == nil {
|
||
return nil, errors.New("渠道模型为空")
|
||
}
|
||
capability := normalizeCapability(channelModel.Capability)
|
||
if capability == "audio" {
|
||
return nil, nil
|
||
}
|
||
if capability != "text" && capability != "image" && capability != "video" {
|
||
return nil, fmt.Errorf("不支持的渠道模型能力:%s", channelModel.Capability)
|
||
}
|
||
config, err := DecodeModelCapabilityConfig(channelModel.CapabilityConfigJSON)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("解析渠道模型能力配置失败:%w", err)
|
||
}
|
||
normalized, err := NormalizeModelCapabilityConfigForModel(capability, string(channelModel.Protocol), firstNonEmpty(channelModel.ProviderModelKey, channelModel.ModelKey), config, registries...)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return normalized, nil
|
||
}
|
||
|
||
func NormalizeModelCapabilityConfig(capability string, protocol string, input *ModelCapabilityConfig) (*ModelCapabilityConfig, error) {
|
||
return NormalizeModelCapabilityConfigForModel(capability, protocol, "", input)
|
||
}
|
||
|
||
func NormalizeModelCapabilityConfigForModel(capability string, protocol string, modelName string, input *ModelCapabilityConfig, registries ...*protocolcontract.Registry) (*ModelCapabilityConfig, error) {
|
||
if capability != "text" && capability != "image" && capability != "video" {
|
||
return nil, nil
|
||
}
|
||
if capability == "text" {
|
||
if input == nil || input.Text == nil {
|
||
return nil, BadAuthRequest("请配置文本模型能力参数")
|
||
}
|
||
text := *input.Text
|
||
if text.Streaming == nil {
|
||
streaming := true
|
||
text.Streaming = &streaming
|
||
}
|
||
if text.ContextWindowTokens == 0 {
|
||
text.ContextWindowTokens = 128000
|
||
}
|
||
if text.MaxOutputTokens == 0 {
|
||
text.MaxOutputTokens = 16384
|
||
}
|
||
value := &ModelCapabilityConfig{Version: 1, Text: &text}
|
||
if err := validateTextCapabilityConfig(value.Text); err != nil {
|
||
return nil, err
|
||
}
|
||
return value, nil
|
||
}
|
||
if capability == "image" {
|
||
if input == nil || input.Image == nil {
|
||
return nil, BadAuthRequest("请配置图片模型能力参数")
|
||
}
|
||
if len(registries) > 0 && registries[0] != nil {
|
||
if adapter, ok := registries[0].Resolve(protocol); ok && adapter.Metadata().ImageParameters != nil {
|
||
if err := resolveImageCapabilityParameters(input.Image, adapter.Metadata().ImageParameters, modelName); err != nil {
|
||
return nil, err
|
||
}
|
||
} else if input.Image.Parameters != nil {
|
||
return nil, BadAuthRequest("当前插件未声明图片参数合同,请先更新插件")
|
||
}
|
||
}
|
||
if input.Image.Parameters != nil {
|
||
if err := projectImageParameters(input.Image); err != nil {
|
||
return nil, err
|
||
}
|
||
} else {
|
||
normalizeLegacyImageResolutionConfig(protocol, input.Image)
|
||
input.Image.ResolutionUsesQuality = strings.TrimSpace(protocol) == string(model.ChannelInterfaceGeminiImage)
|
||
}
|
||
// 旧配置缺换算表时按声明的档位与比例补齐后再校验,保存即把旧线路归一化成当前形态。
|
||
if input.Image.Parameters == nil {
|
||
fillMissingImageSizePresets(input.Image, protocol)
|
||
}
|
||
value := &ModelCapabilityConfig{Version: 1, Image: input.Image}
|
||
if err := validateImageCapabilityConfig(value.Image); err != nil {
|
||
return nil, err
|
||
}
|
||
return value, nil
|
||
}
|
||
if input == nil || input.Video == nil {
|
||
return nil, BadAuthRequest("请配置视频模型能力参数")
|
||
}
|
||
video := applyModelSpecificVideoCapability(input.Video, protocol, modelName)
|
||
if strings.TrimSpace(protocol) == "autodl-comfyui" {
|
||
var err error
|
||
video, err = normalizeAutoDLVideoScreenSpec(video, modelName)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
value := &ModelCapabilityConfig{Version: 1, Video: video}
|
||
if err := validateVideoCapabilityConfig(value.Video); err != nil {
|
||
return nil, err
|
||
}
|
||
return value, nil
|
||
}
|
||
|
||
// normalizeLegacyImageResolutionConfig projects legacy values only for protocols whose
|
||
// shipped contracts prove that these exact quality values represented resolution tiers.
|
||
// The projection is in-memory; persisted capabilities and historical task inputs are untouched.
|
||
func normalizeLegacyImageResolutionConfig(protocol string, image *ImageCapabilityConfig) {
|
||
if image == nil || image.Resolution.Supported || !image.Quality.Supported {
|
||
return
|
||
}
|
||
allowed := legacyImageResolutionTiers(protocol)
|
||
if len(allowed) == 0 {
|
||
return
|
||
}
|
||
values := make([]string, 0, len(image.Quality.Values))
|
||
for _, value := range image.Quality.Values {
|
||
value = strings.ToLower(strings.TrimSpace(value))
|
||
if !allowed[value] {
|
||
return
|
||
}
|
||
values = append(values, value)
|
||
}
|
||
defaultValue := strings.ToLower(strings.TrimSpace(image.Quality.Default))
|
||
if len(values) == 0 || !allowed[defaultValue] || !containsCapabilityString(values, defaultValue) {
|
||
return
|
||
}
|
||
image.Resolution = ImageResolutionConfig{Supported: true, Values: values, Default: defaultValue}
|
||
image.Quality = ImageQualityConfig{Supported: false, Values: []string{}, Default: "auto"}
|
||
}
|
||
|
||
func legacyImageResolutionProtocol(protocol string) bool {
|
||
return len(legacyImageResolutionTiers(protocol)) > 0
|
||
}
|
||
|
||
func legacyImageResolutionTiers(protocol string) map[string]bool {
|
||
switch strings.TrimSpace(protocol) {
|
||
case string(model.ChannelInterfaceGrokImage):
|
||
return map[string]bool{"1k": true, "2k": true}
|
||
case "agnes-image":
|
||
return map[string]bool{"1k": true, "2k": true, "4k": true}
|
||
default:
|
||
return nil
|
||
}
|
||
}
|
||
|
||
// normalizeLegacyImageResolutionRequest adapts only old request fields for the same
|
||
// proven protocols. Other plugins retain their own quality semantics.
|
||
func normalizeLegacyImageResolutionRequest(protocol string, capability *ModelCapabilityConfig, config map[string]any) string {
|
||
if capability == nil || capability.Image == nil || !capability.Image.Resolution.Supported || capability.Image.Quality.Supported {
|
||
return ""
|
||
}
|
||
imageResolutionProtocol := strings.TrimSpace(protocol) == string(model.ChannelInterfaceGrokImage) || strings.TrimSpace(protocol) == "agnes-image"
|
||
if !imageResolutionProtocol {
|
||
return ""
|
||
}
|
||
legacy := strings.TrimSpace(metadataString(config, "quality"))
|
||
if !containsCapabilityString(capability.Image.Resolution.Values, legacy) {
|
||
return ""
|
||
}
|
||
if strings.TrimSpace(metadataString(config, "imageResolution")) != "" {
|
||
delete(config, "quality")
|
||
return ""
|
||
}
|
||
config["imageResolution"] = legacy
|
||
delete(config, "quality")
|
||
return legacy
|
||
}
|
||
|
||
func applyModelSpecificVideoCapability(profile *VideoCapabilityConfig, protocol string, modelName string) *VideoCapabilityConfig {
|
||
if profile == nil || model.ChannelInterfaceType(strings.TrimSpace(protocol)) != model.ChannelInterfaceAgnesVideo {
|
||
return profile
|
||
}
|
||
normalizedModel := strings.ToLower(strings.TrimSpace(modelName))
|
||
if normalizedModel != "agnes-video-2.5" && normalizedModel != "agnes-video-2.5-flash" {
|
||
return profile
|
||
}
|
||
value := *profile
|
||
value.References = profile.References
|
||
flash := normalizedModel == "agnes-video-2.5-flash"
|
||
value.References.MaxImages = 9
|
||
value.References.MaxVideos = 3
|
||
value.References.MaxAudios = 3
|
||
value.References.MaxVideoBytes = 200 * 1024 * 1024
|
||
value.References.MaxVideoDuration = 15
|
||
value.References.MaxAudioBytes = 15 * 1024 * 1024
|
||
value.References.MaxAudioDuration = 15
|
||
if flash {
|
||
value.References.MaxImages = 5
|
||
value.References.MaxVideos = 0
|
||
value.References.MaxVideoBytes = 0
|
||
value.References.MaxVideoDuration = 0
|
||
}
|
||
value.Duration = VideoDurationConfig{Selection: "range", Min: 4, Max: 12, Step: 1, Default: 5}
|
||
value.Ratios = []string{"21:9", "16:9", "4:3", "1:1", "3:4", "9:16"}
|
||
value.DefaultRatio = "16:9"
|
||
value.Resolutions = []string{"720P", "960P", "2K"}
|
||
if flash {
|
||
value.Resolutions = []string{"720P"}
|
||
}
|
||
value.DefaultResolution = "720P"
|
||
value.GenerateAudio = VideoBooleanConfig{Supported: false, Default: false}
|
||
value.Watermark = VideoBooleanConfig{Supported: false, Default: false}
|
||
value.Operations = []string{"text_to_video", "image_to_video", "reference_to_video", "audio_to_video"}
|
||
value.DefaultOperation = "text_to_video"
|
||
return &value
|
||
}
|
||
|
||
// CapabilitySpecFromModelCapabilityConfig 将渠道模型的真实供应能力投影为路由能力规格。
|
||
// 渠道模型能力参数是唯一事实来源,前台模型供应线路直接引用该规格。
|
||
func CapabilitySpecFromModelCapabilityConfig(config *ModelCapabilityConfig, capability string) (CapabilitySpec, error) {
|
||
spec := CapabilitySpec{Version: 1, Capability: capability, Inputs: map[string]InputConstraint{}, Options: map[string]OptionConstraint{}}
|
||
// 音频模型当前没有可编辑的渠道能力 JSON,使用空能力规格表示“无额外路由约束”。
|
||
if capability == "audio" {
|
||
return spec, nil
|
||
}
|
||
if config == nil {
|
||
switch capability {
|
||
case "text":
|
||
return spec, BadAuthRequest("渠道文本模型尚未配置能力参数")
|
||
case "image":
|
||
return spec, BadAuthRequest("渠道图片模型尚未配置能力参数")
|
||
case "video":
|
||
return spec, BadAuthRequest("渠道视频模型尚未配置能力参数")
|
||
default:
|
||
return spec, BadAuthRequest("渠道模型尚未配置能力参数")
|
||
}
|
||
}
|
||
switch capability {
|
||
case "text":
|
||
if config.Text == nil {
|
||
return spec, BadAuthRequest("渠道文本模型尚未配置能力参数")
|
||
}
|
||
addInputConstraint(spec.Inputs, "image", 0, config.Text.References.MaxImages)
|
||
addInputConstraint(spec.Inputs, "video", 0, config.Text.References.MaxVideos)
|
||
case "image":
|
||
if config.Image == nil {
|
||
return spec, BadAuthRequest("渠道图片模型尚未配置能力参数")
|
||
}
|
||
image := config.Image
|
||
addInputConstraint(spec.Inputs, "image", 0, image.References.MaxImages)
|
||
if image.References.MaskSupported {
|
||
addInputConstraint(spec.Inputs, "mask", 0, 1)
|
||
}
|
||
if image.Size.Parameter != "none" {
|
||
spec.Options["size"] = imageSizeOptionConstraint(image.Size)
|
||
spec.ImageSize = capabilityImageSizeFromConfig(image.Size)
|
||
if image.Parameters != nil && !image.Resolution.Supported {
|
||
size := image.Size
|
||
size.Presets = activeImageSizePresets(image)
|
||
spec.ImageSize = capabilityImageSizeFromConfig(size)
|
||
}
|
||
}
|
||
if image.Resolution.Supported {
|
||
spec.Options["imageResolution"] = anyValues(image.Resolution.Values)
|
||
}
|
||
if image.Quality.Supported {
|
||
spec.Options["quality"] = anyValues(image.Quality.Values)
|
||
}
|
||
if image.TransparentBackground.Supported {
|
||
spec.Options["transparentBackground"] = boolValues(true)
|
||
} else {
|
||
spec.Options["transparentBackground"] = boolValues(false)
|
||
}
|
||
spec.Options["count"] = numericRange(1, float64(image.MaxOutputs), 1)
|
||
if image.Parameters != nil {
|
||
spec.Options = imageParameterConstraints(image.Parameters)
|
||
spec.ImageParameters = publicImageParameters(image.Parameters)
|
||
}
|
||
case "video":
|
||
if config.Video == nil {
|
||
return spec, BadAuthRequest("渠道视频模型尚未配置能力参数")
|
||
}
|
||
video := config.Video
|
||
spec.Operations = append([]string(nil), video.Operations...)
|
||
addInputConstraint(spec.Inputs, "image", video.References.MinImages, video.References.MaxImages)
|
||
addInputConstraint(spec.Inputs, "video", 0, video.References.MaxVideos)
|
||
addInputConstraint(spec.Inputs, "audio", 0, video.References.MaxAudios)
|
||
if video.Duration.Selection == "enum" {
|
||
values := make([]any, 0, len(video.Duration.Values))
|
||
for _, value := range video.Duration.Values {
|
||
values = append(values, value)
|
||
}
|
||
spec.Options["videoSeconds"] = OptionConstraint{Values: values}
|
||
} else {
|
||
spec.Options["videoSeconds"] = numericRange(float64(video.Duration.Min), float64(video.Duration.Max), float64(video.Duration.Step))
|
||
}
|
||
if len(video.Ratios) > 0 {
|
||
spec.Options["size"] = anyValues(video.Ratios)
|
||
}
|
||
if len(video.Resolutions) > 0 {
|
||
spec.Options["vquality"] = anyValues(video.Resolutions)
|
||
}
|
||
if video.GenerateAudio.Supported {
|
||
spec.Options["videoGenerateAudio"] = boolValues(true)
|
||
} else {
|
||
spec.Options["videoGenerateAudio"] = boolValues(false)
|
||
}
|
||
if video.Watermark.Supported {
|
||
spec.Options["videoWatermark"] = boolValues(true)
|
||
} else {
|
||
spec.Options["videoWatermark"] = boolValues(false)
|
||
}
|
||
default:
|
||
return spec, BadAuthRequest("未知模型能力类型")
|
||
}
|
||
return spec, nil
|
||
}
|
||
|
||
// imageSizeOptionConstraint 保留可见的标准尺寸/比例,同时用 * 表示允许自定义。
|
||
// * 不能替代标准值,否则管理端只能看到一个没有业务含义的通配符。
|
||
func imageSizeOptionConstraint(size ImageSizeConfig) OptionConstraint {
|
||
values := make([]string, 0, len(size.Values)+1)
|
||
seen := make(map[string]struct{}, len(size.Values)+1)
|
||
for _, value := range size.Values {
|
||
value = strings.TrimSpace(value)
|
||
if value == "" || value == "*" {
|
||
continue
|
||
}
|
||
if _, exists := seen[value]; exists {
|
||
continue
|
||
}
|
||
seen[value] = struct{}{}
|
||
values = append(values, value)
|
||
}
|
||
if size.AllowCustom {
|
||
if len(values) == 0 {
|
||
for _, value := range legacyImageSizeValues() {
|
||
seen[value] = struct{}{}
|
||
values = append(values, value)
|
||
}
|
||
}
|
||
values = append(values, "*")
|
||
}
|
||
return anyValues(values)
|
||
}
|
||
|
||
func capabilityImageSizeFromConfig(size ImageSizeConfig) *CapabilityImageSize {
|
||
if size.Parameter == "" || size.Parameter == "none" {
|
||
return nil
|
||
}
|
||
result := &CapabilityImageSize{Parameter: size.Parameter, AllowCustom: size.AllowCustom}
|
||
for _, preset := range size.Presets {
|
||
tier := strings.ToLower(strings.TrimSpace(preset.Tier))
|
||
ratio := strings.TrimSpace(preset.Ratio)
|
||
value := strings.TrimSpace(preset.Size)
|
||
if ratio == "" || value == "" {
|
||
continue
|
||
}
|
||
result.Presets = append(result.Presets, CapabilityImageSizePreset{
|
||
Size: value, Tier: tier, Ratio: ratio, Width: preset.Width, Height: preset.Height,
|
||
})
|
||
}
|
||
return result
|
||
}
|
||
|
||
func addInputConstraint(inputs map[string]InputConstraint, name string, min int, max int) {
|
||
if min <= 0 && max <= 0 {
|
||
return
|
||
}
|
||
inputs[name] = InputConstraint{Min: min, Max: max}
|
||
}
|