182 lines
5.5 KiB
Go
182 lines
5.5 KiB
Go
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))
|
||
}
|