Agent Plan Seedream 与 Seedance 共用 /api/plan/v3,BaseURL 判断会把图片生成误判为视频可信素材路径。仅视频 mode/协议才同步或提交 asset://。 Co-authored-by: Cursor <cursoragent@cursor.com>
304 lines
7.1 KiB
Go
304 lines
7.1 KiB
Go
package app
|
||
|
||
import (
|
||
"context"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"infinite-canvas/backend/internal/model"
|
||
)
|
||
|
||
func isPublicMediaURL(value string) bool {
|
||
lower := strings.ToLower(value)
|
||
return strings.HasPrefix(lower, "http://") || strings.HasPrefix(lower, "https://")
|
||
}
|
||
|
||
func isSeedanceVideoConfig(config providerConfig) bool {
|
||
modelName := strings.ToLower(config.Model)
|
||
return strings.Contains(modelName, "seedance") || strings.Contains(modelName, "doubao-seedance") || isArkPlanVideoConfig(config)
|
||
}
|
||
|
||
func isGrokVideoConfig(config providerConfig) bool {
|
||
return strings.Contains(strings.ToLower(strings.TrimSpace(config.Model)), "grok")
|
||
}
|
||
|
||
func isArkPlanVideoConfig(config providerConfig) bool {
|
||
if !strings.Contains(strings.ToLower(config.BaseURL), "/api/plan/v3") {
|
||
return false
|
||
}
|
||
// Agent Plan 图片与视频共用 /api/plan/v3;按协议排除图片,避免 Seedream 误走视频/可信素材路径。
|
||
iface := strings.TrimSpace(config.InterfaceType)
|
||
if iface == string(model.ChannelInterfaceVolcengineArkImage) || iface == "volcengine-ark-agent-plan-image" {
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
func normalizeImageQuality(value string) string {
|
||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||
case "1k":
|
||
return "low"
|
||
case "2k":
|
||
return "medium"
|
||
case "4k":
|
||
return "high"
|
||
default:
|
||
return value
|
||
}
|
||
}
|
||
|
||
func imageParameterSupported(profile *ImageCapabilityConfig, parameter string) bool {
|
||
if profile == nil {
|
||
return true
|
||
}
|
||
if parameter == "response_format" {
|
||
return profile.ResponseFormat.Supported
|
||
}
|
||
return profile.OutputFormat.Supported
|
||
}
|
||
|
||
func imageQualitySupported(profile *ImageCapabilityConfig) bool {
|
||
return profile == nil || profile.Quality.Supported
|
||
}
|
||
|
||
func imageTransparentBackgroundSupported(profile *ImageCapabilityConfig) bool {
|
||
return profile == nil || profile.TransparentBackground.Supported
|
||
}
|
||
|
||
func imageSizeParameter(profile *ImageCapabilityConfig, value string) (string, string) {
|
||
if profile == nil {
|
||
return "size", normalizePixelSize(value)
|
||
}
|
||
value = strings.TrimSpace(value)
|
||
if strings.EqualFold(value, "auto") {
|
||
return "", ""
|
||
}
|
||
if value == "" {
|
||
value = strings.TrimSpace(profile.Size.Default)
|
||
}
|
||
switch profile.Size.Parameter {
|
||
case "size":
|
||
return "size", normalizePixelSize(value)
|
||
case "aspect_ratio":
|
||
return "aspect_ratio", normalizeImageAspectRatio(value)
|
||
default:
|
||
return "", ""
|
||
}
|
||
}
|
||
|
||
func normalizeImageAspectRatio(value string) string {
|
||
value = strings.TrimSpace(strings.ToLower(strings.ReplaceAll(value, "×", "x")))
|
||
if strings.Contains(value, ":") {
|
||
return value
|
||
}
|
||
parts := strings.Split(value, "x")
|
||
if len(parts) != 2 {
|
||
return ""
|
||
}
|
||
width, widthErr := strconv.Atoi(parts[0])
|
||
height, heightErr := strconv.Atoi(parts[1])
|
||
if widthErr != nil || heightErr != nil || width <= 0 || height <= 0 {
|
||
return ""
|
||
}
|
||
divisor := imageDimensionGCD(width, height)
|
||
return strconv.Itoa(width/divisor) + ":" + strconv.Itoa(height/divisor)
|
||
}
|
||
|
||
func imageDimensionGCD(left int, right int) int {
|
||
for right != 0 {
|
||
left, right = right, left%right
|
||
}
|
||
if left < 1 {
|
||
return 1
|
||
}
|
||
return left
|
||
}
|
||
|
||
func normalizePixelSize(value string) string {
|
||
value = strings.TrimSpace(value)
|
||
if value == "" || value == "auto" {
|
||
return ""
|
||
}
|
||
// 画布按比例保存常用预设;图片接口只接受像素尺寸,必须在请求边界完成转换。
|
||
switch value {
|
||
case "1:1":
|
||
return "1024x1024"
|
||
case "3:2":
|
||
return "1536x1024"
|
||
case "2:3":
|
||
return "1024x1536"
|
||
case "4:3":
|
||
return "1360x1024"
|
||
case "3:4":
|
||
return "1024x1360"
|
||
case "16:9":
|
||
return "1824x1024"
|
||
case "9:16":
|
||
return "1024x1824"
|
||
case "21:9":
|
||
return "2352x1008"
|
||
}
|
||
if strings.Contains(value, "x") {
|
||
return value
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func normalizeVideoSize(value string) string {
|
||
value = strings.TrimSpace(value)
|
||
if value == "" || value == "auto" {
|
||
return ""
|
||
}
|
||
if strings.Contains(value, "x") {
|
||
return value
|
||
}
|
||
if value == "9:16" || value == "2:3" || value == "3:4" {
|
||
return "720x1280"
|
||
}
|
||
return "1280x720"
|
||
}
|
||
|
||
func normalizeVideoResolution(value string) string {
|
||
value = strings.TrimSpace(value)
|
||
if value == "" || value == "auto" || value == "medium" || value == "high" {
|
||
return "720p"
|
||
}
|
||
if value == "low" {
|
||
return "480p"
|
||
}
|
||
if strings.EqualFold(value, "4k") {
|
||
return "2160p"
|
||
}
|
||
if strings.EqualFold(value, "2k") {
|
||
return "1440p"
|
||
}
|
||
if strings.HasSuffix(value, "p") {
|
||
return value
|
||
}
|
||
return value + "p"
|
||
}
|
||
|
||
func videoResolutionNameRequest(profile *VideoCapabilityConfig, value string) string {
|
||
requested := strings.ToLower(strings.TrimSpace(value))
|
||
if requested == "" || requested == "auto" || requested == "default" || requested == "medium" || requested == "high" {
|
||
return ""
|
||
}
|
||
if profile == nil {
|
||
return ""
|
||
}
|
||
if len(profile.Resolutions) == 0 {
|
||
return ""
|
||
}
|
||
candidates := []string{requested, strings.ToLower(normalizeVideoResolution(requested))}
|
||
if requested == "4k" {
|
||
candidates = append(candidates, "2160p")
|
||
}
|
||
if requested == "2160" || requested == "2160p" {
|
||
candidates = append(candidates, "4k")
|
||
}
|
||
for _, supported := range profile.Resolutions {
|
||
for _, candidate := range candidates {
|
||
if strings.EqualFold(strings.TrimSpace(supported), candidate) {
|
||
return strings.TrimSpace(supported)
|
||
}
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func isAutomaticVideoResolution(value string) bool {
|
||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||
case "", "auto", "default", "medium", "high":
|
||
return true
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
|
||
func normalizeSeedanceDuration(value string) int {
|
||
if strings.TrimSpace(value) == "-1" {
|
||
return -1
|
||
}
|
||
seconds, err := strconv.Atoi(strings.TrimSpace(value))
|
||
if err != nil || seconds <= 0 {
|
||
return 5
|
||
}
|
||
return seconds
|
||
}
|
||
|
||
func normalizeSeedanceVideosDuration(value string) int {
|
||
return normalizeSeedanceDuration(value)
|
||
}
|
||
|
||
func normalizeSeedanceRatio(value string) string {
|
||
value = strings.TrimSpace(value)
|
||
if value == "" || value == "auto" || value == "adaptive" {
|
||
return "adaptive"
|
||
}
|
||
switch value {
|
||
case "16:9", "9:16", "1:1", "4:3", "3:4", "21:9":
|
||
return value
|
||
default:
|
||
return "adaptive"
|
||
}
|
||
}
|
||
|
||
func normalizeSeedanceVideosRatio(value string) string {
|
||
ratio := normalizeSeedanceRatio(value)
|
||
if ratio == "adaptive" {
|
||
return "16:9"
|
||
}
|
||
return ratio
|
||
}
|
||
|
||
func normalizeSeedanceResolution(value string, model string) string {
|
||
resolution := strings.TrimSuffix(strings.TrimSpace(value), "p")
|
||
if strings.EqualFold(resolution, "4k") {
|
||
resolution = "2160"
|
||
}
|
||
switch resolution {
|
||
case "480", "720", "1080", "2160":
|
||
default:
|
||
if value == "low" {
|
||
resolution = "480"
|
||
} else {
|
||
resolution = "720"
|
||
}
|
||
}
|
||
if strings.Contains(strings.ToLower(model), "fast") && (resolution == "1080" || resolution == "2160") {
|
||
resolution = "720"
|
||
}
|
||
return resolution + "p"
|
||
}
|
||
|
||
func parseBool(value string, fallback bool) bool {
|
||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||
case "true":
|
||
return true
|
||
case "false":
|
||
return false
|
||
default:
|
||
return fallback
|
||
}
|
||
}
|
||
|
||
func parseFloat(value string, fallback float64) float64 {
|
||
number, err := strconv.ParseFloat(strings.TrimSpace(value), 64)
|
||
if err != nil || number == 0 {
|
||
return fallback
|
||
}
|
||
return number
|
||
}
|
||
|
||
func sleepContext(ctx context.Context, duration time.Duration) error {
|
||
timer := time.NewTimer(duration)
|
||
defer timer.Stop()
|
||
select {
|
||
case <-ctx.Done():
|
||
return ctx.Err()
|
||
case <-timer.C:
|
||
return nil
|
||
}
|
||
}
|