fix(media): 规范比例与 OpenAI 图像尺寸参数
统一过滤非 x:x 比例,避免默认值参与候选匹配或透传上游。 支持 xK 分辨率归一化,并按 Gemini、Volces 和 OpenAI 的参数规范生成上游请求;OpenAI 显式 WxH 始终保留用户原始尺寸。 验证:在独立临时 worktree 中执行 apps/api 全量 go test ./... -count=1 通过,真实 OpenAI 请求已到达上游但受本地无效凭据阻断。
This commit is contained in:
@@ -3,6 +3,7 @@ package runner
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
|
||||
@@ -13,28 +14,43 @@ type resolutionNormalizeProcessor struct{}
|
||||
func (resolutionNormalizeProcessor) Name() string { return "ResolutionNormalizeProcessor" }
|
||||
|
||||
func (resolutionNormalizeProcessor) ShouldProcess(params map[string]any, modelType string, context *paramProcessContext) bool {
|
||||
if stringFromAny(params["resolution"]) != "" {
|
||||
return false
|
||||
if normalizeMediaResolution(modelType, stringFromAny(params["resolution"])) != "" {
|
||||
return true
|
||||
}
|
||||
size := stringFromAny(params["size"])
|
||||
if size == "" {
|
||||
return false
|
||||
}
|
||||
return isImageResolution(modelType, size) || isVideoResolution(modelType, size)
|
||||
return normalizeMediaResolution(modelType, stringFromAny(params["size"])) != ""
|
||||
}
|
||||
|
||||
func (resolutionNormalizeProcessor) Process(params map[string]any, modelType string, context *paramProcessContext) bool {
|
||||
if resolution := normalizeMediaResolution(modelType, stringFromAny(params["resolution"])); resolution != "" {
|
||||
before := params["resolution"]
|
||||
params["resolution"] = resolution
|
||||
context.resolution = resolution
|
||||
if stringFromAny(before) != resolution {
|
||||
_, capabilityValue := capabilityEvidence(context.modelCapability, modelType, "output_resolutions")
|
||||
context.recordChange(
|
||||
"ResolutionNormalizeProcessor",
|
||||
"adjust",
|
||||
"resolution",
|
||||
before,
|
||||
resolution,
|
||||
"resolution 使用了可识别的媒体分辨率格式,已归一为标准大小写和值。",
|
||||
capabilityPath(modelType, "output_resolutions"),
|
||||
capabilityValue,
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
size := stringFromAny(params["size"])
|
||||
if stringFromAny(params["resolution"]) == "" && (isImageResolution(modelType, size) || isVideoResolution(modelType, size)) {
|
||||
if resolution := normalizeMediaResolution(modelType, size); resolution != "" {
|
||||
_, capabilityValue := capabilityEvidence(context.modelCapability, modelType, "output_resolutions")
|
||||
params["resolution"] = size
|
||||
context.resolution = size
|
||||
params["resolution"] = resolution
|
||||
context.resolution = resolution
|
||||
context.recordChange(
|
||||
"ResolutionNormalizeProcessor",
|
||||
"set",
|
||||
"resolution",
|
||||
nil,
|
||||
size,
|
||||
resolution,
|
||||
"size 使用分辨率格式,归一到 resolution 供后续能力校验和计费使用。",
|
||||
capabilityPath(modelType, "output_resolutions"),
|
||||
capabilityValue,
|
||||
@@ -48,30 +64,69 @@ type aspectRatioProcessor struct{}
|
||||
func (aspectRatioProcessor) Name() string { return "AspectRatioProcessor" }
|
||||
|
||||
func (aspectRatioProcessor) ShouldProcess(params map[string]any, modelType string, context *paramProcessContext) bool {
|
||||
return modelType != "text_generate" && (stringFromAny(params["aspect_ratio"]) != "" || stringFromAny(params["size"]) != "")
|
||||
if modelType == "text_generate" {
|
||||
return false
|
||||
}
|
||||
for _, key := range []string{"aspect_ratio", "aspectRatio", "ratio"} {
|
||||
if _, ok := params[key]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return stringFromAny(params["size"]) != ""
|
||||
}
|
||||
|
||||
func (aspectRatioProcessor) Process(params map[string]any, modelType string, context *paramProcessContext) bool {
|
||||
capability := capabilityForType(context.modelCapability, modelType)
|
||||
if capability == nil {
|
||||
aspectRatio, ratioSource, ratioPresent := requestAspectRatio(params)
|
||||
if ratioPresent {
|
||||
originalAspectRatio := aspectRatio
|
||||
normalized, valid := normalizedAspectRatio(aspectRatio)
|
||||
if !valid {
|
||||
before := map[string]any{}
|
||||
for _, key := range []string{"aspect_ratio", "aspectRatio", "ratio"} {
|
||||
if value, ok := params[key]; ok {
|
||||
before[key] = cloneAny(value)
|
||||
delete(params, key)
|
||||
}
|
||||
}
|
||||
aspectRatio = ""
|
||||
context.aspectRatio = ""
|
||||
context.recordChange(
|
||||
"AspectRatioProcessor",
|
||||
"remove",
|
||||
ratioSource,
|
||||
before,
|
||||
nil,
|
||||
"比例不是有效的正数 x:x 格式,按未指定比例处理。",
|
||||
"",
|
||||
nil,
|
||||
)
|
||||
return true
|
||||
} else {
|
||||
aspectRatio = normalized
|
||||
params["aspect_ratio"] = normalized
|
||||
for _, key := range []string{"aspectRatio", "ratio"} {
|
||||
delete(params, key)
|
||||
}
|
||||
if ratioSource != "aspect_ratio" || normalized != originalAspectRatio {
|
||||
context.recordChange(
|
||||
"AspectRatioProcessor",
|
||||
"adjust",
|
||||
"aspect_ratio",
|
||||
map[string]any{ratioSource: originalAspectRatio},
|
||||
normalized,
|
||||
"比例已归一为标准 x:x 格式。",
|
||||
"",
|
||||
nil,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !ratioPresent {
|
||||
return true
|
||||
}
|
||||
|
||||
aspectRatio := stringFromAny(params["aspect_ratio"])
|
||||
if isEmptyParamString(aspectRatio) {
|
||||
before := params["aspect_ratio"]
|
||||
delete(params, "aspect_ratio")
|
||||
context.aspectRatio = ""
|
||||
context.recordChange(
|
||||
"AspectRatioProcessor",
|
||||
"remove",
|
||||
"aspect_ratio",
|
||||
before,
|
||||
nil,
|
||||
"aspect_ratio 是空值字符串,不能作为有效比例传给上游。",
|
||||
"",
|
||||
nil,
|
||||
)
|
||||
capability := capabilityForType(context.modelCapability, modelType)
|
||||
if capability == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -174,6 +229,15 @@ func (aspectRatioProcessor) Process(params map[string]any, modelType string, con
|
||||
return true
|
||||
}
|
||||
|
||||
func requestAspectRatio(params map[string]any) (string, string, bool) {
|
||||
for _, key := range []string{"aspect_ratio", "aspectRatio", "ratio"} {
|
||||
if value, ok := params[key]; ok {
|
||||
return stringFromAny(value), key, true
|
||||
}
|
||||
}
|
||||
return "", "aspect_ratio", false
|
||||
}
|
||||
|
||||
func validateExplicitAspectRatio(aspectRatio string, capability map[string]any, allowed []string, modelType string) (string, bool) {
|
||||
if isVideoModelType(modelType) && len(allowed) > 0 && !containsString(allowed, aspectRatio) {
|
||||
if aspectRatio == "adaptive" || aspectRatio == "keep_ratio" {
|
||||
@@ -194,17 +258,14 @@ func (imageSizeProcessor) ShouldProcess(params map[string]any, modelType string,
|
||||
if modelType != "image_generate" && modelType != "image_edit" {
|
||||
return false
|
||||
}
|
||||
if _, _, ok := imageDimensionsFromParams(params); !ok {
|
||||
return false
|
||||
}
|
||||
capability := capabilityForType(context.modelCapability, modelType)
|
||||
return capability != nil && imageSizeCapabilityConfigured(capability)
|
||||
_, _, ok := imageDimensionsFromParams(params)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (imageSizeProcessor) Process(params map[string]any, modelType string, context *paramProcessContext) bool {
|
||||
capability := capabilityForType(context.modelCapability, modelType)
|
||||
if capability == nil {
|
||||
return true
|
||||
capability = map[string]any{}
|
||||
}
|
||||
width, height, ok := imageDimensionsFromParams(params)
|
||||
if !ok {
|
||||
@@ -222,7 +283,11 @@ func (imageSizeProcessor) Process(params map[string]any, modelType string, conte
|
||||
params["width"] = width
|
||||
params["height"] = height
|
||||
_, _, sizeHasPixelDimensions := parsePixelSizeString(stringFromAny(params["size"]))
|
||||
if stringFromAny(params["aspect_ratio"]) == "" || sizeHasPixelDimensions {
|
||||
_, _, resolutionHasPixelDimensions := parsePixelSizeString(stringFromAny(params["resolution"]))
|
||||
explicitWidthHeight := positiveIntegerFromAny(params["width"]) > 0 && positiveIntegerFromAny(params["height"]) > 0 &&
|
||||
normalizeImageResolution(stringFromAny(params["size"])) == "" &&
|
||||
normalizeImageResolution(stringFromAny(params["resolution"])) == ""
|
||||
if sizeHasPixelDimensions || resolutionHasPixelDimensions || (stringFromAny(params["aspect_ratio"]) == "" && explicitWidthHeight) {
|
||||
aspectRatio := aspectRatioFromDimensions(width, height)
|
||||
allowed := aspectRatioAllowed(capability["aspect_ratio_allowed"], firstNonEmptyString(stringFromAny(params["resolution"]), context.resolution))
|
||||
if processed, ok := validateAndAdjustAspectRatio(aspectRatio, capability, allowed); ok && processed != "" {
|
||||
@@ -274,7 +339,37 @@ func imageDimensionsFromParams(params map[string]any) (int, int, bool) {
|
||||
if width > 0 && height > 0 {
|
||||
return width, height, true
|
||||
}
|
||||
return parsePixelSizeString(stringFromAny(params["resolution"]))
|
||||
if width, height, ok := parsePixelSizeString(stringFromAny(params["resolution"])); ok {
|
||||
return width, height, true
|
||||
}
|
||||
resolution := firstNonEmptyString(
|
||||
normalizeImageResolution(stringFromAny(params["resolution"])),
|
||||
normalizeImageResolution(stringFromAny(params["size"])),
|
||||
)
|
||||
if resolution == "" {
|
||||
return 0, 0, false
|
||||
}
|
||||
return imageDimensionsForResolution(resolution, stringFromAny(params["aspect_ratio"]))
|
||||
}
|
||||
|
||||
func imageDimensionsForResolution(resolution string, aspectRatio string) (int, int, bool) {
|
||||
resolution = normalizeImageResolution(resolution)
|
||||
if resolution == "" {
|
||||
return 0, 0, false
|
||||
}
|
||||
magnitude, err := strconv.Atoi(strings.TrimSuffix(resolution, "K"))
|
||||
if err != nil || magnitude <= 0 {
|
||||
return 0, 0, false
|
||||
}
|
||||
baseSide := magnitude * 1024
|
||||
ratio, validRatio := aspectRatioNumber(aspectRatio)
|
||||
if !validRatio {
|
||||
return baseSide, baseSide, true
|
||||
}
|
||||
if ratio >= 1 {
|
||||
return baseSide, max(1, int(math.Round(float64(baseSide)/ratio))), true
|
||||
}
|
||||
return max(1, int(math.Round(float64(baseSide)*ratio))), baseSide, true
|
||||
}
|
||||
|
||||
func imageSizeCapabilityConfigured(capability map[string]any) bool {
|
||||
|
||||
Reference in New Issue
Block a user