512 lines
12 KiB
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
|
|
}
|
|
}
|