sub2api/backend/internal/service/image_billing_size.go
2026-05-12 15:21:31 +08:00

261 lines
6.1 KiB
Go

package service
import (
"sort"
"strconv"
"strings"
)
const (
ImageBillingSize1K = "1K"
ImageBillingSize2K = "2K"
ImageBillingSize4K = "4K"
ImageSizeSourceOutput = "output"
ImageSizeSourceInput = "input"
ImageSizeSourceDefault = "default"
ImageSizeSourceLegacy = "legacy"
)
type ImageBillingSizeResolution struct {
BillingSize string
InputSize string
OutputSize string
Source string
Breakdown map[string]int
}
func ClassifyImageBillingTier(size string) (string, bool) {
trimmed := strings.TrimSpace(size)
normalized := strings.ToLower(trimmed)
switch normalized {
case "", "auto":
return "", false
case "1k":
return ImageBillingSize1K, true
case "2k":
return ImageBillingSize2K, true
case "4k":
return ImageBillingSize4K, true
case "2048x2048", "2048x1152":
return ImageBillingSize2K, true
case "3840x2160", "2160x3840":
return ImageBillingSize4K, true
}
width, height, ok := parseImageBillingDimensions(trimmed)
if !ok {
return "", false
}
maxEdge := width
if height > maxEdge {
maxEdge = height
}
switch {
case maxEdge <= 1024:
return ImageBillingSize1K, true
case maxEdge <= 2048:
return ImageBillingSize2K, true
default:
return ImageBillingSize4K, true
}
}
func NormalizeImageBillingTierOrDefault(size string) string {
if tier, ok := ClassifyImageBillingTier(size); ok {
return tier
}
return ImageBillingSize2K
}
func ResolveImageBillingSize(inputSize string, outputSizes []string) ImageBillingSizeResolution {
inputSize = strings.TrimSpace(inputSize)
outputSizes = compactTrimmedStrings(outputSizes)
breakdown := map[string]int{}
outputSize := firstDisplayImageOutputSize(outputSizes)
outputTier := ""
for _, output := range outputSizes {
tier, ok := ClassifyImageBillingTier(output)
if !ok {
continue
}
breakdown[tier]++
if imageTierRank(tier) > imageTierRank(outputTier) {
outputTier = tier
}
}
if outputTier != "" {
return ImageBillingSizeResolution{
BillingSize: outputTier,
InputSize: inputSize,
OutputSize: outputSize,
Source: ImageSizeSourceOutput,
Breakdown: normalizeImageSizeBreakdown(breakdown),
}
}
if tier, ok := ClassifyImageBillingTier(inputSize); ok {
return ImageBillingSizeResolution{
BillingSize: tier,
InputSize: inputSize,
OutputSize: outputSize,
Source: ImageSizeSourceInput,
}
}
return ImageBillingSizeResolution{
BillingSize: ImageBillingSize2K,
InputSize: inputSize,
OutputSize: outputSize,
Source: ImageSizeSourceDefault,
}
}
func ApplyOpenAIImageBillingResolution(result *OpenAIForwardResult) {
if result == nil || result.ImageCount <= 0 {
return
}
inputSize := strings.TrimSpace(result.ImageInputSize)
if inputSize == "" && strings.TrimSpace(result.ImageSize) != ImageBillingSize2K {
inputSize = strings.TrimSpace(result.ImageSize)
}
outputSizes := result.ImageOutputSizes
if len(outputSizes) == 0 && strings.TrimSpace(result.ImageOutputSize) != "" {
outputSizes = []string{result.ImageOutputSize}
}
resolved := ResolveImageBillingSize(inputSize, outputSizes)
applyImageBillingResolution(
&result.ImageSize,
&result.ImageInputSize,
&result.ImageOutputSize,
&result.ImageSizeSource,
&result.ImageSizeBreakdown,
resolved,
)
}
func ApplyForwardImageBillingResolution(result *ForwardResult) {
if result == nil || result.ImageCount <= 0 {
return
}
inputSize := strings.TrimSpace(result.ImageInputSize)
if inputSize == "" && strings.TrimSpace(result.ImageSize) != ImageBillingSize2K {
inputSize = strings.TrimSpace(result.ImageSize)
}
outputSizes := result.ImageOutputSizes
if len(outputSizes) == 0 && strings.TrimSpace(result.ImageOutputSize) != "" {
outputSizes = []string{result.ImageOutputSize}
}
resolved := ResolveImageBillingSize(inputSize, outputSizes)
applyImageBillingResolution(
&result.ImageSize,
&result.ImageInputSize,
&result.ImageOutputSize,
&result.ImageSizeSource,
&result.ImageSizeBreakdown,
resolved,
)
}
func applyImageBillingResolution(
billingSize *string,
inputSize *string,
outputSize *string,
source *string,
breakdown *map[string]int,
resolved ImageBillingSizeResolution,
) {
*billingSize = resolved.BillingSize
*inputSize = resolved.InputSize
*outputSize = resolved.OutputSize
*source = resolved.Source
*breakdown = resolved.Breakdown
}
func parseImageBillingDimensions(size string) (int, int, bool) {
parts := strings.Split(strings.ToLower(strings.TrimSpace(size)), "x")
if len(parts) != 2 {
return 0, 0, false
}
width, err := strconv.Atoi(strings.TrimSpace(parts[0]))
if err != nil {
return 0, 0, false
}
height, err := strconv.Atoi(strings.TrimSpace(parts[1]))
if err != nil {
return 0, 0, false
}
if width <= 0 || height <= 0 {
return 0, 0, false
}
return width, height, true
}
func compactTrimmedStrings(values []string) []string {
if len(values) == 0 {
return nil
}
out := make([]string, 0, len(values))
for _, value := range values {
trimmed := strings.TrimSpace(value)
if trimmed != "" {
out = append(out, trimmed)
}
}
return out
}
func firstDisplayImageOutputSize(outputSizes []string) string {
for _, output := range outputSizes {
if trimmed := strings.TrimSpace(output); trimmed != "" {
return trimmed
}
}
return ""
}
func imageTierRank(tier string) int {
switch strings.ToUpper(strings.TrimSpace(tier)) {
case ImageBillingSize1K:
return 1
case ImageBillingSize2K:
return 2
case ImageBillingSize4K:
return 3
default:
return 0
}
}
func normalizeImageSizeBreakdown(in map[string]int) map[string]int {
if len(in) == 0 {
return nil
}
out := make(map[string]int, len(in))
for _, tier := range []string{ImageBillingSize1K, ImageBillingSize2K, ImageBillingSize4K} {
if count := in[tier]; count > 0 {
out[tier] = count
}
}
if len(out) == 0 {
return nil
}
return out
}
func SortedImageBillingBreakdownKeys(breakdown map[string]int) []string {
keys := make([]string, 0, len(breakdown))
for key := range breakdown {
keys = append(keys, key)
}
sort.Slice(keys, func(i, j int) bool {
left, right := imageTierRank(keys[i]), imageTierRank(keys[j])
if left == right {
return keys[i] < keys[j]
}
return left < right
})
return keys
}