Files
ttstd-infinite-canvas/backend/internal/app/image_parameters.go

541 lines
17 KiB
Go

package app
import (
"encoding/json"
"fmt"
"log"
"strconv"
"strings"
"yingce/backend/internal/model"
"yingce/backend/internal/protocol"
)
func resolveImageCapabilityParameters(image *ImageCapabilityConfig, definitions []protocol.ImageParameter, model string) error {
configured := image.Parameters
if configured == nil {
// Import existing declarations once. A resolution-only plugin may import
// old quality tiers only when every value is explicitly in its contract.
hasQuality := false
for _, p := range definitions {
if p.Name == "quality" && p.AppliesTo(model) {
hasQuality = true
}
}
for _, definition := range definitions {
if !definition.AppliesTo(model) {
continue
}
p := definition
enabled := p.IsEnabled()
var values []string
var initial string
switch p.Name {
case "size":
enabled = image.Size.Parameter != "none"
values, initial = image.Size.Values, image.Size.Default
case "imageResolution":
enabled = image.Resolution.Supported
values, initial = image.Resolution.Values, image.Resolution.Default
if !enabled && !hasQuality && image.Quality.Supported {
valid := len(image.Quality.Values) > 0
for _, value := range image.Quality.Values {
valid = valid && definition.Accepts(value)
}
if valid {
enabled, values, initial = true, image.Quality.Values, image.Quality.Default
}
}
case "quality":
enabled, values, initial = image.Quality.Supported, image.Quality.Values, image.Quality.Default
case "transparentBackground":
enabled, p.Default = image.TransparentBackground.Supported, image.TransparentBackground.Default
case "count":
maximum := float64(image.MaxOutputs)
if p.Max == nil || maximum < *p.Max {
p.Max = &maximum
}
}
if len(values) > 0 {
p.Values = nil
for _, value := range values {
if definition.Accepts(value) {
p.Values = append(p.Values, value)
}
}
if len(p.Values) == 0 {
enabled = false
}
if p.Accepts(initial) {
p.Default = initial
} else if len(p.Values) > 0 {
p.Default = p.Values[0]
}
}
p.Enabled = &enabled
configured = append(configured, p)
}
}
parameters, err := protocol.ResolveImageParameters(definitions, configured, model)
if err != nil {
return BadAuthRequest("图片参数配置无效:" + err.Error())
}
image.Parameters = parameters
return projectImageParameters(image)
}
// Legacy structs are an execution projection, never a second editable source.
func projectImageParameters(image *ImageCapabilityConfig) error {
if err := protocol.ValidateImageParameters(image.Parameters); err != nil {
return BadAuthRequest(err.Error())
}
image.Quality = ImageQualityConfig{Default: "auto", Values: []string{}}
image.Resolution = ImageResolutionConfig{Default: "auto", Values: []string{}}
image.TransparentBackground = VideoBooleanConfig{}
image.ResolutionUsesQuality = false
image.MaxOutputs = 1
sizeEnabled := false
for _, p := range image.Parameters {
if !p.IsEnabled() {
continue
}
values := make([]string, 0, len(p.Values))
for _, value := range p.Values {
values = append(values, fmt.Sprint(value))
}
initial := "auto"
if p.Default != nil {
initial = fmt.Sprint(p.Default)
} else if len(values) > 0 {
initial = values[0]
}
switch p.Name {
case "size":
sizeEnabled = true
image.Size.Parameter = "aspect_ratio"
if p.Encoding == "pixels" {
image.Size.Parameter = "size"
for _, value := range values {
if strings.Contains(value, ":") {
image.Size.Parameter = "aspect_ratio"
break
}
}
}
image.Size.Values, image.Size.Default = values, initial
image.Size.AllowCustom = len(p.Values) == 0
case "imageResolution":
image.Resolution = ImageResolutionConfig{Supported: true, Values: values, Default: initial}
case "quality":
image.Quality = ImageQualityConfig{Supported: true, Values: values, Default: initial}
case "transparentBackground":
image.TransparentBackground = VideoBooleanConfig{Supported: true, Default: p.Default == true}
case "count":
if p.Max != nil {
image.MaxOutputs = int(*p.Max)
}
}
}
if !sizeEnabled {
image.Size = ImageSizeConfig{Parameter: "none", Default: "auto", Values: []string{}}
}
if sizeEnabled {
presets := make([]ImageSizePreset, 0, len(image.Size.Presets))
for _, preset := range image.Size.Presets {
value := preset.Ratio
if image.Size.Parameter == "size" {
value = preset.Size
}
if (image.Size.AllowCustom || containsCapabilityString(image.Size.Values, value)) && (!image.Resolution.Supported || len(image.Resolution.Values) == 0 || containsCapabilityString(image.Resolution.Values, preset.Tier)) {
presets = append(presets, preset)
}
}
image.Size.Presets = presets
}
return nil
}
func publicImageParameters(parameters []protocol.ImageParameter) []protocol.ImageParameter {
result := make([]protocol.ImageParameter, 0, len(parameters))
for _, p := range parameters {
p.Binding, p.Models = "", nil
p.Encoding = ""
result = append(result, p)
}
return result
}
// Disabled resolution keeps its presets for editing; execution uses one explicit
// default group. Multiple groups without a default must not pick by array order.
func singleImageSizeTier(image *ImageCapabilityConfig) (string, bool) {
if image.Size.DefaultTier != "" {
return image.Size.DefaultTier, true
}
for _, field := range image.Parameters {
if field.Name == "imageResolution" && field.Default != nil && field.Default != "auto" {
return fmt.Sprint(field.Default), true
}
}
for _, preset := range image.Size.Presets {
if preset.Size == image.Size.Default {
return preset.Tier, true
}
}
if len(image.Size.Presets) == 0 {
return "", true
}
tier := image.Size.Presets[0].Tier
for _, preset := range image.Size.Presets {
if preset.Tier != tier {
return "", false
}
}
return tier, true
}
func activeImageSizePresets(image *ImageCapabilityConfig) []ImageSizePreset {
if image.Parameters == nil || image.Resolution.Supported {
return image.Size.Presets
}
tier, ok := singleImageSizeTier(image)
if !ok {
return nil
}
result := make([]ImageSizePreset, 0, len(image.Size.Presets))
for _, preset := range image.Size.Presets {
if preset.Tier == tier {
result = append(result, preset)
}
}
return result
}
func imageParameterConstraints(parameters []protocol.ImageParameter) map[string]OptionConstraint {
result := map[string]OptionConstraint{}
for _, p := range parameters {
if !p.IsEnabled() {
continue
}
constraint := OptionConstraint{Values: p.Values, Min: p.Min, Max: p.Max, Step: p.Step}
if len(p.Values) > 0 {
constraint.Min, constraint.Max, constraint.Step = nil, nil, nil
}
if len(p.Values) == 0 && p.Type == "boolean" {
constraint.Values = []any{false, true}
}
if len(p.Values) == 0 && p.Type == "string" {
constraint.Values = []any{"*"}
}
if len(p.Values) == 0 && (p.Type == "number" || p.Type == "integer") && (p.Min == nil || p.Max == nil) {
constraint = OptionConstraint{Values: []any{"*"}}
}
result[p.Name] = constraint
}
return result
}
func imageParameterDefaults(parameters []protocol.ImageParameter) map[string]any {
result := map[string]any{}
for _, p := range parameters {
if p.IsEnabled() && p.Default != nil {
result[p.Name] = p.Default
}
}
return result
}
func imageParameterValue(p protocol.ImageParameter, value any) any {
text, ok := value.(string)
if !ok || p.Type == "string" {
return value
}
switch p.Type {
case "boolean":
if v, err := strconv.ParseBool(text); err == nil {
return v
}
case "number", "integer":
if v, err := strconv.ParseFloat(text, 64); err == nil {
return v
}
}
return value
}
func imageParameterAdmissionError(field, issue, message string) error {
return NewModelError(ErrCodeModelCapabilityNotSupported, message).WithDetails(map[string]any{
"field": field,
"issue": issue,
"message": message,
})
}
func resolveImageParameterValues(parameters []protocol.ImageParameter, values map[string]any) (map[string]any, error) {
allowed := map[string]protocol.ImageParameter{}
result := imageParameterDefaults(parameters)
for _, p := range parameters {
if p.IsEnabled() {
allowed[p.Name] = p
}
}
for name, value := range values {
p, ok := allowed[name]
if !ok {
return nil, imageParameterAdmissionError(name, "unsupported_value", "当前图片模型未启用参数:"+name)
}
value = imageParameterValue(p, value)
if !p.Accepts(value) {
return nil, imageParameterAdmissionError(p.Name, "unsupported_value", "图片参数超出支持范围:"+p.Label)
}
if value != nil && value != "" {
result[name] = value
}
}
for _, p := range parameters {
if p.IsEnabled() && p.Required && (result[p.Name] == nil || result[p.Name] == "") {
return nil, imageParameterAdmissionError(p.Name, "required", "请填写图片参数:"+p.Label)
}
}
return result, nil
}
func validateImageParameterPriceSelector(parameters []protocol.ImageParameter, selector map[string]string) error {
allowed := map[string]protocol.ImageParameter{}
for _, p := range parameters {
if p.IsEnabled() && p.Pricing {
allowed[p.Name] = p
}
}
for name, value := range selector {
if name == "operation" {
if value != "text_to_image" && value != "image_to_image" && value != "" && value != "*" {
return BadAuthRequest("图片价格生成方式无效")
}
continue
}
p, ok := allowed[name]
if !ok {
return BadAuthRequest("当前图片模型未声明此计价参数:" + name)
}
if value == "" || value == "*" {
continue
}
if !p.Accepts(imageParameterValue(p, value)) {
return BadAuthRequest("价格档参数不在支持范围内:" + p.Label)
}
}
return nil
}
func declaredImageParameterValues(config map[string]any, parameters []protocol.ImageParameter) map[string]any {
values := map[string]any{}
for _, p := range parameters {
if !p.IsEnabled() {
continue
}
if value, exists := config[p.Name]; exists && value != nil && strings.TrimSpace(fmt.Sprint(value)) != "" {
values[p.Name] = value
}
}
if extras, ok := config["imageParameters"].(map[string]any); ok {
for name, value := range extras {
values[name] = value
}
}
if encoded, ok := config["imageParameters"].(string); ok && encoded != "" {
var extras map[string]any
if json.Unmarshal([]byte(encoded), &extras) == nil {
for name, value := range extras {
values[name] = value
}
} else {
values["invalidImageParameters"] = encoded
}
}
return values
}
// Admission and worker execution use the same validated values. Core fields are
// only a transport projection; unknown or disabled extension values fail here.
func applyImageParameterValues(config *providerConfig, profile *ImageCapabilityConfig) error {
if profile == nil || profile.Parameters == nil {
return nil
}
values := declaredImageParameterValues(map[string]any{
"size": config.Size, "imageResolution": config.ImageResolution, "quality": config.Quality,
"count": config.Count, "transparentBackground": config.TransparentBackground, "imageParameters": config.ImageParameters,
}, profile.Parameters)
for _, field := range profile.Parameters {
if field.Name == "size" && !field.Accepts(values["size"]) {
size, resolution := fmt.Sprint(values["size"]), config.ImageResolution
if pixels := resolveImageTaskSize(profile, config.InterfaceType, size, resolution); field.Encoding == "pixels" && field.Accepts(pixels) {
values["size"] = pixels
} else if ratio := imageSizeRatioForDeclaredPreset(profile, size, resolution); ratio != "" && field.Accepts(ratio) {
values["size"] = ratio
}
}
}
resolved, err := resolveImageParameterValues(profile.Parameters, values)
if err != nil {
return err
}
config.ImageParameters = resolved
text := func(name string) string {
if value, exists := resolved[name]; exists {
return fmt.Sprint(value)
}
return ""
}
config.Size, config.ImageResolution, config.Quality = text("size"), text("imageResolution"), text("quality")
config.Count, config.TransparentBackground = text("count"), text("transparentBackground")
if imageSizePixelOutbound(profile, config.InterfaceType) {
config.Size = resolveImageTaskSize(profile, config.InterfaceType, config.Size, config.ImageResolution)
if strings.Contains(config.Size, ":") {
return BadAuthRequest("当前比例与分辨率缺少明确的像素尺寸映射")
}
}
return nil
}
func imagePriceSelectorForParameters(parameters []protocol.ImageParameter, intent ModelRequestIntent) map[string]string {
selector := map[string]string{"operation": "text_to_image"}
if intent.Inputs["image"] > 0 {
selector["operation"] = "image_to_image"
}
for _, p := range parameters {
if value, ok := intent.Options[p.Name]; ok && p.IsEnabled() && p.Pricing && value != nil {
selector[p.Name] = fmt.Sprint(value)
}
}
return selector
}
func imagePriceParametersForChannel(channelModel model.ChannelModel, specs ...CapabilitySpec) []protocol.ImageParameter {
if len(specs) > 0 {
return specs[0].ImageParameters
}
config, err := DecodeModelCapabilityConfig(channelModel.CapabilityConfigJSON)
if err == nil && config != nil && config.Image != nil {
return config.Image.Parameters
}
return nil
}
// Read projection follows live route declarations. Explicit omissions in a new
// frontend schema remain omissions; a legacy snapshot imports available fields
// once. The saved route graph and administrator restrictions remain untouched.
func effectiveImageParameterSpec(spec CapabilitySpec, routes []CapabilitySpec) CapabilitySpec {
if spec.Capability != "image" {
return spec
}
sources := map[string]protocol.ImageParameter{}
conflicts := map[string]bool{}
order := []string{}
for _, route := range routes {
for _, p := range route.ImageParameters {
if !p.IsEnabled() {
continue
}
previous, exists := sources[p.Name]
if !exists {
sources[p.Name] = p
order = append(order, p.Name)
continue
}
if previous.Type != p.Type {
conflicts[p.Name] = true
continue
}
previous.Pricing = previous.Pricing && p.Pricing
if len(previous.Values) > 0 && len(p.Values) > 0 {
for _, candidate := range p.Values {
found := false
for _, value := range previous.Values {
found = found || protocol.ImageParameterValuesEqual(candidate, value)
}
if !found {
previous.Values = append(previous.Values, candidate)
}
}
} else if p.Min != nil && p.Max != nil && previous.Min != nil && previous.Max != nil {
minimum, maximum := min(*p.Min, *previous.Min), max(*p.Max, *previous.Max)
candidate := OptionConstraint{Min: &minimum, Max: &maximum, Step: previous.Step}
if optionConstraintCovered(candidate, p.Name, routes) {
previous.Min, previous.Max = &minimum, &maximum
}
}
sources[p.Name] = previous
}
}
hasContract := false
for _, route := range routes {
hasContract = hasContract || route.ImageParameters != nil
}
if !hasContract {
return spec
}
result := spec
result.ImageParameters = []protocol.ImageParameter{}
result.Options = map[string]OptionConstraint{}
for _, name := range order {
if conflicts[name] {
log.Printf("image parameter omitted because route types conflict: %s", name)
continue
}
p := sources[name]
constraint, selected := spec.Options[name]
if !selected {
known := false
for _, previous := range spec.ImageParameters {
known = known || previous.Name == name
}
if known {
disabled := false
p.Enabled = &disabled
result.ImageParameters = append(result.ImageParameters, p)
continue
}
}
if selected {
if len(constraint.Values) > 0 && !isWildcardOptionConstraint(constraint) {
values := []any{}
for _, value := range constraint.Values {
if p.Accepts(imageParameterValue(p, value)) {
values = append(values, imageParameterValue(p, value))
}
}
if len(values) == 0 {
continue
}
p.Values = values
}
if constraint.Min != nil && (p.Min == nil || *constraint.Min > *p.Min) {
p.Min = constraint.Min
}
if constraint.Max != nil && (p.Max == nil || *constraint.Max < *p.Max) {
p.Max = constraint.Max
}
if constraint.Step != nil && (p.Step == nil || *constraint.Step >= *p.Step) {
p.Step = constraint.Step
}
if p.Min != nil && p.Max != nil && *p.Min > *p.Max {
continue
}
}
if p.Default != nil && !p.Accepts(p.Default) {
p.Default = nil
if len(p.Values) > 0 {
p.Default = p.Values[0]
}
}
p.Binding, p.Models, p.Encoding = "", nil, ""
result.ImageParameters = append(result.ImageParameters, p)
}
result.Options = imageParameterConstraints(result.ImageParameters)
result.ImageSize = mergeCapabilityImageSize(routes)
if _, ok := result.Options["size"]; !ok {
result.ImageSize = nil
}
return result
}