topfans/backend/gateway/service/imageseg_client.go
2026-06-03 22:19:22 +08:00

182 lines
5.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package service
import (
"context"
"crypto/hmac"
"crypto/sha1"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"sort"
"strings"
"time"
"github.com/google/uuid"
"github.com/topfans/backend/gateway/config"
)
// ImagesegClient 视觉智能开放平台 — 分割抠图imageseg
type ImagesegClient struct {
accessKeyID string
accessKeySecret string
regionID string
client *http.Client
}
func NewImagesegClient(ossCfg config.OSSConfig) *ImagesegClient {
region := strings.TrimSpace(ossCfg.Region)
region = strings.TrimPrefix(region, "oss-")
if region == "" {
region = "cn-shanghai"
}
return &ImagesegClient{
accessKeyID: strings.TrimSpace(ossCfg.AccessKeyID),
accessKeySecret: strings.TrimSpace(ossCfg.AccessKeySecret),
regionID: region,
client: &http.Client{Timeout: 120 * time.Second},
}
}
func (c *ImagesegClient) enabled() bool {
return c.accessKeyID != "" && c.accessKeySecret != "" && c.accessKeyID != "your-access-key-id"
}
// SegmentHDBodyURL 人体高清抠图,返回结果图 URL对应控制台已开通的 SegmentHDBody
func (c *ImagesegClient) SegmentHDBodyURL(ctx context.Context, imageURL string) (string, error) {
return c.callPOP(ctx, "SegmentHDBody", imageURL)
}
// SegmentCommonImageURL 通用分割抠图(异形主体可备选)
func (c *ImagesegClient) SegmentCommonImageURL(ctx context.Context, imageURL string) (string, error) {
return c.callPOP(ctx, "SegmentCommonImage", imageURL)
}
func (c *ImagesegClient) callPOP(ctx context.Context, action, imageURL string) (string, error) {
if !c.enabled() {
return "", fmt.Errorf("分割抠图未配置 OSS AccessKey")
}
imageURL = strings.TrimSpace(imageURL)
if !strings.HasPrefix(imageURL, "http://") && !strings.HasPrefix(imageURL, "https://") {
return "", fmt.Errorf("分割抠图需要可访问的图片 URL")
}
params := map[string]string{
"Format": "JSON",
"Version": "2019-12-30",
"AccessKeyId": c.accessKeyID,
"SignatureMethod": "HMAC-SHA1",
"Timestamp": time.Now().UTC().Format("2006-01-02T15:04:05Z"),
"SignatureVersion": "1.0",
"SignatureNonce": uuid.NewString(),
"Action": action,
"ImageURL": imageURL,
}
keys := make([]string, 0, len(params))
for k := range params {
keys = append(keys, k)
}
sort.Strings(keys)
pairs := make([]string, 0, len(keys))
for _, k := range keys {
pairs = append(pairs, percentEncode(k)+"="+percentEncode(params[k]))
}
canonicalized := strings.Join(pairs, "&")
stringToSign := "POST&" + percentEncode("/") + "&" + percentEncode(canonicalized)
mac := hmac.New(sha1.New, []byte(c.accessKeySecret+"&"))
mac.Write([]byte(stringToSign))
signature := base64.StdEncoding.EncodeToString(mac.Sum(nil))
body := canonicalized + "&Signature=" + percentEncode(signature)
host := fmt.Sprintf("imageseg.%s.aliyuncs.com", c.regionID)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://"+host+"/", strings.NewReader(body))
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
res, err := c.client.Do(req)
if err != nil {
return "", err
}
defer res.Body.Close()
raw, err := io.ReadAll(io.LimitReader(res.Body, 1<<20))
if err != nil {
return "", err
}
if res.StatusCode < 200 || res.StatusCode >= 300 {
return "", mapImagesegHTTPError(res.StatusCode, string(raw))
}
var data map[string]interface{}
if err := json.Unmarshal(raw, &data); err != nil {
return "", fmt.Errorf("分割抠图返回非 JSON")
}
if code, ok := data["Code"]; ok {
if s := fmt.Sprint(code); s != "" && s != "0" && !strings.EqualFold(s, "success") {
msg, _ := data["Message"].(string)
return "", mapImagesegBizError(s, msg)
}
}
flat := data
if inner, ok := data["Data"].(string); ok && inner != "" {
var parsed map[string]interface{}
if err := json.Unmarshal([]byte(inner), &parsed); err == nil {
for k, v := range parsed {
flat[k] = v
}
}
}
if inner, ok := data["Data"].(map[string]interface{}); ok {
for k, v := range inner {
flat[k] = v
}
}
outURL := pickImagesegResultURL(flat)
if outURL == "" {
return "", fmt.Errorf("分割抠图未返回结果图 URL")
}
return outURL, nil
}
func pickImagesegResultURL(data map[string]interface{}) string {
for _, key := range []string{"ImageURL", "imageURL", "Url", "url", "ResultImageURL"} {
if u, ok := data[key].(string); ok && isHTTPURL(u) {
return u
}
}
return pickModelScopeImageURL(data)
}
func mapImagesegBizError(code, message string) error {
lower := strings.ToLower(message)
if code == "InvalidApi.NotPurchase" || strings.Contains(lower, "not purchase") || strings.Contains(lower, "notpurchase") {
return fmt.Errorf("分割抠图能力未开通:请在视觉智能开放平台控制台确认 SegmentHDBody 已开通华东2-上海)")
}
if code == "Forbidden" || strings.Contains(lower, "no permission") {
return fmt.Errorf("分割抠图无权限:请为 RAM 用户添加 AliyunVIAPIFullAccess 策略")
}
if message != "" {
return fmt.Errorf("分割抠图 %s: %s", code, message)
}
return fmt.Errorf("分割抠图错误: %s", code)
}
func mapImagesegHTTPError(httpCode int, raw string) error {
var data map[string]interface{}
if err := json.Unmarshal([]byte(raw), &data); err == nil {
code := fmt.Sprint(data["Code"])
msg, _ := data["Message"].(string)
if code != "" && code != "<nil>" {
return mapImagesegBizError(code, msg)
}
}
return fmt.Errorf("分割抠图 HTTP %d: %s", httpCode, truncate(raw, 240))
}