package runner import ( "math" "sort" "strconv" "strings" ) func validateAndAdjustAspectRatio(aspectRatio string, capability map[string]any, allowed []string) (string, bool) { if !isMediaModelTypeWithAspectRatio(capability) { return "", false } if ratioRange, ok := numberPair(capability["aspect_ratio_range"]); ok { ratio, valid := aspectRatioNumber(aspectRatio) if !valid || ratio < ratioRange[0] || ratio > ratioRange[1] { return adjustAspectRatioToRange(aspectRatio, ratioRange[0], ratioRange[1], allowed), true } } if allowed == nil { return aspectRatio, true } if len(allowed) == 0 { return "", false } if (aspectRatio == "adaptive" || aspectRatio == "keep_ratio") && !containsString(allowed, aspectRatio) { return "", false } if containsString(allowed, aspectRatio) { return aspectRatio, true } return allowed[0], true } func isMediaModelTypeWithAspectRatio(capability map[string]any) bool { return capability != nil } func aspectRatioAllowed(value any, resolution string) []string { switch typed := value.(type) { case []any: return stringListFromAny(typed) case []string: return typed case map[string]any: if resolution != "" { if values := stringListFromAny(typed[resolution]); len(values) > 0 { return values } } return nil default: return nil } } func scopedNumberList(value any, scopes ...string) []float64 { switch typed := value.(type) { case []any: out := make([]float64, 0, len(typed)) for _, item := range typed { if number := floatFromAny(item); number > 0 { out = append(out, number) } } return out case []float64: return typed case []int: out := make([]float64, 0, len(typed)) for _, item := range typed { out = append(out, float64(item)) } return out case map[string]any: for _, scope := range scopes { if scope == "" { continue } if values := scopedNumberList(typed[scope]); len(values) > 0 { return values } } for _, item := range typed { if values := scopedNumberList(item); len(values) > 0 { return values } } } return nil } func scopedRange(value any, scopes ...string) (float64, float64, bool) { if pair, ok := numberPair(value); ok { return pair[0], pair[1], true } if typed, ok := value.(map[string]any); ok { for _, scope := range scopes { if scope == "" { continue } if minValue, maxValue, ok := scopedRange(typed[scope]); ok { return minValue, maxValue, true } } for _, item := range typed { if minValue, maxValue, ok := scopedRange(item); ok { return minValue, maxValue, true } } } return 0, 0, false } func durationStep(value any, scopes ...string) float64 { if step := floatFromAny(value); step > 0 { return step } if typed, ok := value.(map[string]any); ok { for _, scope := range scopes { if scope == "" { continue } if step := durationStep(typed[scope]); step > 0 { return step } } for _, item := range typed { if step := durationStep(item); step > 0 { return step } } } return 0 } func normalizeDurationByRange(target float64, minValue float64, maxValue float64, step float64) float64 { if minValue > maxValue { minValue, maxValue = maxValue, minValue } if step <= 0 { step = 1 } clamped := math.Min(math.Max(target, minValue), maxValue) snapped := math.Ceil(((clamped-minValue)/step)-1e-9)*step + minValue snapped = math.Min(math.Max(snapped, minValue), maxValue) return math.Round(snapped*1_000_000) / 1_000_000 } func normalizeDurationByStep(target float64, step float64) float64 { if step <= 0 { step = 1 } snapped := math.Ceil((target/step)-1e-9) * step return math.Round(snapped*1_000_000) / 1_000_000 } func nextAllowedNumber(target float64, values []float64) float64 { if len(values) == 0 { return target } sorted := append([]float64(nil), values...) sort.Float64s(sorted) for _, value := range sorted { if value >= target || math.Abs(value-target) < 1e-9 { return value } } return sorted[len(sorted)-1] } func contentItems(value any) []map[string]any { switch typed := value.(type) { case []any: out := make([]map[string]any, 0, len(typed)) for _, item := range typed { if object, ok := item.(map[string]any); ok { out = append(out, cloneMap(object)) } } return out case []map[string]any: out := make([]map[string]any, 0, len(typed)) for _, item := range typed { out = append(out, cloneMap(item)) } return out default: return nil } } func mapsToAnySlice(values []map[string]any) []any { out := make([]any, 0, len(values)) for _, value := range values { out = append(out, value) } return out } func isImageContent(item map[string]any) bool { return stringFromAny(item["type"]) == "image_url" || item["image_url"] != nil } func isVideoContent(item map[string]any) bool { return stringFromAny(item["type"]) == "video_url" || item["video_url"] != nil } func isAudioContent(item map[string]any) bool { return stringFromAny(item["type"]) == "audio_url" || item["audio_url"] != nil } func capabilityForType(capabilities map[string]any, modelType string) map[string]any { if capabilities == nil { return nil } if typed, ok := capabilities[modelType].(map[string]any); ok { return typed } return nil } func capabilityPath(modelType string, key string) string { modelType = strings.TrimSpace(modelType) if modelType == "" { return "" } if strings.TrimSpace(key) == "" { return "capabilities." + modelType } return "capabilities." + modelType + "." + key } func capabilityValue(capabilities map[string]any, modelType string, key string) any { capability := capabilityForType(capabilities, modelType) if capability == nil { return nil } if strings.TrimSpace(key) == "" { return cloneMap(capability) } return cloneAny(capability[key]) } func capabilityEvidence(capabilities map[string]any, modelType string, key string) (string, any) { return capabilityPath(modelType, key), capabilityValue(capabilities, modelType, key) } func audioInputCapabilityEvidence(context *paramProcessContext, modelType string) (string, any) { if isOmniVideoLike(context) { path, value := omniCapabilityEvidence(context, "input_audio") return path, mergeMetrics(map[string]any{"input_audio": value}, omniCapabilityBundle(context, "max_audios")) } return capabilityEvidence(context.modelCapability, modelType, "input_audio") } func omniCapabilityType(context *paramProcessContext) string { if context != nil && capabilityForType(context.modelCapability, "omni_video") != nil { return "omni_video" } if context != nil && capabilityForType(context.modelCapability, "omni") != nil { return "omni" } return "omni_video" } func omniCapabilityEvidence(context *paramProcessContext, key string) (string, any) { modelType := omniCapabilityType(context) var capabilities map[string]any if context != nil { capabilities = context.modelCapability } return capabilityPath(modelType, key), capabilityValue(capabilities, modelType, key) } func omniCapabilityBundle(context *paramProcessContext, keys ...string) map[string]any { modelType := omniCapabilityType(context) var capabilities map[string]any if context != nil { capabilities = context.modelCapability } out := map[string]any{} for _, key := range keys { out[key] = capabilityValue(capabilities, modelType, key) } return out } func numericField(values map[string]any, key string) (float64, bool) { if values == nil { return 0, false } if _, ok := values[key]; !ok { return 0, false } return floatFromAny(values[key]), true } func boolFromAny(value any) bool { typed, _ := value.(bool) return typed } func firstNonEmptyStringValue(values map[string]any, keys ...string) string { for _, key := range keys { if value := stringFromAny(values[key]); value != "" { return value } } return "" } func firstNonEmptyStringListFromAny(values ...any) []string { for _, value := range values { items := stringListFromAny(value) if len(items) > 0 { return items } } return nil } func stringListFromAny(value any) []string { switch typed := value.(type) { case []string: out := make([]string, 0, len(typed)) for _, item := range typed { if text := strings.TrimSpace(item); text != "" { out = append(out, text) } } return out case []any: out := make([]string, 0, len(typed)) for _, item := range typed { if text := stringFromAny(item); text != "" { out = append(out, text) } } return out case string: if strings.TrimSpace(typed) == "" { return nil } return []string{strings.TrimSpace(typed)} default: return nil } } func containsString(values []string, target string) bool { for _, value := range values { if value == target { return true } } return false } func appendUniqueString(values *[]string, value string) { value = strings.TrimSpace(value) if value == "" { return } for _, existing := range *values { if existing == value { return } } *values = append(*values, value) } func numberPair(value any) ([2]float64, bool) { switch typed := value.(type) { case []any: if len(typed) < 2 { return [2]float64{}, false } return [2]float64{floatFromAny(typed[0]), floatFromAny(typed[1])}, true case []float64: if len(typed) < 2 { return [2]float64{}, false } return [2]float64{typed[0], typed[1]}, true case []int: if len(typed) < 2 { return [2]float64{}, false } return [2]float64{float64(typed[0]), float64(typed[1])}, true default: return [2]float64{}, false } } func validAspectRatio(value string) bool { if value == "adaptive" || value == "keep_ratio" { return true } _, ok := aspectRatioNumber(value) return ok } func aspectRatioNumber(value string) (float64, bool) { parts := strings.Split(value, ":") if len(parts) != 2 { return 0, false } width := parsePositiveFloat(parts[0]) height := parsePositiveFloat(parts[1]) if width <= 0 || height <= 0 { return 0, false } return width / height, true } func adjustAspectRatioToRange(value string, minValue float64, maxValue float64, allowed []string) string { current, ok := aspectRatioNumber(value) if !ok { if len(allowed) > 0 { return allowed[0] } return "1:1" } if len(allowed) > 0 { closest := "" minDiff := math.Inf(1) for _, candidate := range allowed { ratio, ok := aspectRatioNumber(candidate) if !ok || ratio < minValue || ratio > maxValue { continue } diff := math.Abs(ratio - current) if diff < minDiff { minDiff = diff closest = candidate } } if closest != "" { return closest } } if current < minValue { return ratioString(minValue) } return ratioString(maxValue) } func ratioString(value float64) string { if value <= 0 { return "1:1" } return strings.TrimRight(strings.TrimRight(strconv.FormatFloat(value, 'f', 6, 64), "0"), ".") + ":1" } func parsePositiveFloat(value string) float64 { for _, r := range strings.TrimSpace(value) { if r < '0' || r > '9' { if r != '.' { return 0 } } } out, _ := strconv.ParseFloat(strings.TrimSpace(value), 64) return out } func isEmptyParamString(value string) bool { normalized := strings.ToLower(strings.TrimSpace(value)) return normalized == "null" || normalized == "undefined" } func isImageResolution(modelType string, value string) bool { return (modelType == "image_generate" || modelType == "image_edit") && containsString([]string{"1K", "2K", "4K", "8K"}, value) } func isVideoResolution(modelType string, value string) bool { return isVideoModelType(modelType) && containsString([]string{"480p", "720p", "1080p", "1440p", "2160p"}, value) } func isVideoModelType(modelType string) bool { return modelType == "video_generate" || modelType == "text_to_video" || modelType == "image_to_video" || modelType == "video_edit" || modelType == "video_reference" || modelType == "video_first_last_frame" || modelType == "omni_video" || modelType == "omni" } func cloneMap(values map[string]any) map[string]any { out := map[string]any{} for key, value := range values { out[key] = cloneAny(value) } return out } func cloneAny(value any) any { switch typed := value.(type) { case map[string]any: return cloneMap(typed) case []any: out := make([]any, 0, len(typed)) for _, item := range typed { out = append(out, cloneAny(item)) } return out case []map[string]any: out := make([]any, 0, len(typed)) for _, item := range typed { out = append(out, cloneMap(item)) } return out default: return value } }