easyai-ai-gateway/apps/api/internal/runner/param_processor_utils.go

512 lines
12 KiB
Go

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
}
}