The server now keeps portable image ratio normalization, enforces documented Seedream size presets, routes MiniMax img2img through generations-style payloads, and submits Hailuo video variants through the video generations API with proper reference-image handling. Constraint: The deployed test chain relies on popiartServer as the lowest-risk place to adapt provider-specific routing without broad newapi changes Rejected: Expand newapi relay modes for every MiniMax image/video branch | larger blast radius than needed for the verified server-managed flows Confidence: high Scope-risk: moderate Reversibility: clean Directive: Keep MiniMax-specific media routing in popiartServer unless upstream newapi adapters are upgraded to the same contract Tested: go test ./internal/server -run TestInferRouteKeyForModelRecognizesViduAsVideo|TestGenerate|TestResolve|TestSubmitMiniMaxVideoTask|TestExecuteImageToImageJobUsesGenerationsPathForMiniMax|TestExecuteImageToVideoJob|TestResolveVideoReferencesSupportsImagesArray Not-tested: Full PopiNewAPI unit suite (blocked earlier by external module checksum drift in local checkout)
198 lines
4.0 KiB
Go
198 lines
4.0 KiB
Go
package server
|
||
|
||
import (
|
||
"fmt"
|
||
"strconv"
|
||
"strings"
|
||
)
|
||
|
||
var imageAspectRatioSeparators = strings.NewReplacer(
|
||
":", ":",
|
||
"/", ":",
|
||
"/", ":",
|
||
"×", ":",
|
||
"*", ":",
|
||
"x", ":",
|
||
"X", ":",
|
||
)
|
||
|
||
var canonicalImageAspectRatioLabels = map[string]string{
|
||
"7:3": "21:9",
|
||
"16:9": "16:9",
|
||
"4:3": "4:3",
|
||
"3:2": "3:2",
|
||
"1:1": "1:1",
|
||
"9:16": "9:16",
|
||
"3:4": "3:4",
|
||
"2:3": "2:3",
|
||
"5:4": "5:4",
|
||
"4:5": "4:5",
|
||
}
|
||
|
||
var seedreamAspectRatioSizes = map[string]string{
|
||
"21:9": "2520x1080",
|
||
"16:9": "2048x1152",
|
||
"4:3": "2048x1536",
|
||
"3:2": "2304x1536",
|
||
"1:1": "2048x2048",
|
||
"9:16": "1152x2048",
|
||
"3:4": "1536x2048",
|
||
"2:3": "1536x2304",
|
||
"5:4": "1920x1536",
|
||
"4:5": "1536x1920",
|
||
}
|
||
|
||
var exactImageSizeAspectRatios = map[string]string{
|
||
"1024x1024": "1:1",
|
||
"1920x1920": "1:1",
|
||
"1536x1024": "3:2",
|
||
"1024x1536": "2:3",
|
||
"1792x1024": "16:9",
|
||
"1024x1792": "9:16",
|
||
}
|
||
|
||
func normalizeImageAspectRatio(value string) string {
|
||
width, height, ok := parseAspectRatioDimensions(value)
|
||
if !ok {
|
||
return strings.TrimSpace(value)
|
||
}
|
||
return canonicalAspectRatioLabel(width, height)
|
||
}
|
||
|
||
func parseAspectRatioDimensions(value string) (int, int, bool) {
|
||
value = strings.TrimSpace(value)
|
||
if value == "" {
|
||
return 0, 0, false
|
||
}
|
||
|
||
normalized := imageAspectRatioSeparators.Replace(value)
|
||
parts := strings.Split(normalized, ":")
|
||
if len(parts) != 2 {
|
||
return 0, 0, false
|
||
}
|
||
|
||
width, errWidth := strconv.Atoi(strings.TrimSpace(parts[0]))
|
||
height, errHeight := strconv.Atoi(strings.TrimSpace(parts[1]))
|
||
if errWidth != nil || errHeight != nil || width <= 0 || height <= 0 {
|
||
return 0, 0, false
|
||
}
|
||
return width, height, true
|
||
}
|
||
|
||
func canonicalAspectRatioLabel(width, height int) string {
|
||
rw, rh := reduceImageRatio(width, height)
|
||
key := fmt.Sprintf("%d:%d", rw, rh)
|
||
if label, ok := canonicalImageAspectRatioLabels[key]; ok {
|
||
return label
|
||
}
|
||
return key
|
||
}
|
||
|
||
func reduceImageRatio(width, height int) (int, int) {
|
||
g := imageGCD(width, height)
|
||
if g == 0 {
|
||
return width, height
|
||
}
|
||
return width / g, height / g
|
||
}
|
||
|
||
func imageGCD(a, b int) int {
|
||
for b != 0 {
|
||
a, b = b, a%b
|
||
}
|
||
if a < 0 {
|
||
return -a
|
||
}
|
||
return a
|
||
}
|
||
|
||
func parseExactImageSize(value string) (int, int, bool) {
|
||
value = strings.TrimSpace(strings.ToLower(value))
|
||
if value == "" {
|
||
return 0, 0, false
|
||
}
|
||
parts := strings.Split(value, "x")
|
||
if len(parts) != 2 {
|
||
return 0, 0, false
|
||
}
|
||
width, errWidth := strconv.Atoi(strings.TrimSpace(parts[0]))
|
||
height, errHeight := strconv.Atoi(strings.TrimSpace(parts[1]))
|
||
if errWidth != nil || errHeight != nil || width <= 0 || height <= 0 {
|
||
return 0, 0, false
|
||
}
|
||
return width, height, true
|
||
}
|
||
|
||
func normalizeImageSizeToken(value string) string {
|
||
value = strings.TrimSpace(value)
|
||
if value == "" {
|
||
return ""
|
||
}
|
||
switch strings.ToUpper(value) {
|
||
case "1K", "2K", "4K":
|
||
return strings.ToUpper(value)
|
||
}
|
||
width, height, ok := parseExactImageSize(value)
|
||
if !ok {
|
||
return value
|
||
}
|
||
return fmt.Sprintf("%dx%d", width, height)
|
||
}
|
||
|
||
func deriveAspectRatioFromImageSize(value string) string {
|
||
value = normalizeImageSizeToken(value)
|
||
if ratio, ok := exactImageSizeAspectRatios[value]; ok {
|
||
return ratio
|
||
}
|
||
width, height, ok := parseExactImageSize(value)
|
||
if !ok {
|
||
if ratio := normalizeImageAspectRatio(value); ratio != "" {
|
||
return ratio
|
||
}
|
||
return ""
|
||
}
|
||
return canonicalAspectRatioLabel(width, height)
|
||
}
|
||
|
||
func geminiImageSizeFromValue(value string) string {
|
||
value = normalizeImageSizeToken(value)
|
||
switch value {
|
||
case "1K", "2K", "4K":
|
||
return value
|
||
}
|
||
width, height, ok := parseExactImageSize(value)
|
||
if !ok {
|
||
return ""
|
||
}
|
||
longEdge := width
|
||
if height > longEdge {
|
||
longEdge = height
|
||
}
|
||
switch {
|
||
case longEdge <= 1024:
|
||
return "1K"
|
||
case longEdge <= 2560:
|
||
return "2K"
|
||
default:
|
||
return "4K"
|
||
}
|
||
}
|
||
|
||
func seedreamImageSizeFromAspectRatio(value string) string {
|
||
if ratio := normalizeImageAspectRatio(value); ratio != "" {
|
||
if size, ok := seedreamAspectRatioSizes[ratio]; ok {
|
||
return size
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func supportedSeedreamSize(modelID, value string) bool {
|
||
value = strings.ToUpper(strings.TrimSpace(value))
|
||
if value == "" {
|
||
return false
|
||
}
|
||
_, ok := supportedSeedreamSizes(modelID)[value]
|
||
return ok
|
||
}
|