feat: 完善minimax调用图生图功能
This commit is contained in:
parent
6a0f4cc26a
commit
baf56d5ecf
@ -48,3 +48,6 @@ OSS_TOKEN_EXPIRE_TIME=3600
|
|||||||
ENV=development
|
ENV=development
|
||||||
# 日志级别: debug, info, warn, error
|
# 日志级别: debug, info, warn, error
|
||||||
LOG_LEVEL=info
|
LOG_LEVEL=info
|
||||||
|
# ==================== MiniMax API Configuration ====================
|
||||||
|
MINIMAX_API_KEY=
|
||||||
|
MINIMAX_API_URL=https://api.minimaxi.com/v1/image_generation
|
||||||
|
|||||||
@ -50,7 +50,7 @@ func NewAssetController(dubboClient *client.Client) (*AssetController, error) {
|
|||||||
|
|
||||||
return &AssetController{
|
return &AssetController{
|
||||||
assetService: assetService,
|
assetService: assetService,
|
||||||
minimaxService: service.NewMinimaxService(nil),
|
minimaxService: service.NewMinimaxService(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1411,15 +1411,15 @@ func (ctrl *AssetController) listOSSImageFiles(
|
|||||||
return files, nil
|
return files, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ImageGeneration 创建图生图任务
|
// ImageGeneration 图生图(同步调用)
|
||||||
// @Summary 图生图
|
// @Summary 图生图
|
||||||
// @Description 创建图生图任务
|
// @Description 调用 MiniMax 图生图 API
|
||||||
// @Tags assets
|
// @Tags assets
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
// @Param request body dto.ImageGenerationRequest true "图生图请求"
|
// @Param request body dto.ImageGenerationRequest true "图生图请求"
|
||||||
// @Success 202 {object} response.Response
|
// @Success 200 {object} response.Response
|
||||||
// @Router /api/v1/assets/mints/image/generation [post]
|
// @Router /api/v1/assets/mints/image/generation [post]
|
||||||
func (ctrl *AssetController) ImageGeneration(c *gin.Context) {
|
func (ctrl *AssetController) ImageGeneration(c *gin.Context) {
|
||||||
var req dto.ImageGenerationRequest
|
var req dto.ImageGenerationRequest
|
||||||
@ -1428,63 +1428,13 @@ func (ctrl *AssetController) ImageGeneration(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
userID := c.GetInt64("userID")
|
result, err := ctrl.minimaxService.GenerateImage(c.Request.Context(), &req)
|
||||||
starID := c.GetInt64("starID")
|
|
||||||
|
|
||||||
job, err := ctrl.minimaxService.CreateJob(c.Request.Context(), userID, starID, &req)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.Error(c, 500, "Failed to create job: "+err.Error())
|
response.Error(c, 500, "Image generation failed: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(202, gin.H{"code": 202, "message": "ok", "data": &dto.ImageJobCreateResponse{
|
response.Success(c, gin.H{
|
||||||
JobID: job.JobID,
|
"images": result.Images,
|
||||||
Status: string(job.Status),
|
|
||||||
CreatedAt: job.CreatedAt,
|
|
||||||
}})
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetImageJob 查询图生图任务状态
|
|
||||||
// @Summary 查询图生图任务
|
|
||||||
// @Description 查询图生图任务状态和结果
|
|
||||||
// @Tags assets
|
|
||||||
// @Produce json
|
|
||||||
// @Security BearerAuth
|
|
||||||
// @Param job_id path string true "任务ID"
|
|
||||||
// @Success 200 {object} response.Response
|
|
||||||
// @Router /api/v1/assets/mints/image/generation/{job_id} [get]
|
|
||||||
func (ctrl *AssetController) GetImageJob(c *gin.Context) {
|
|
||||||
jobID := c.Param("job_id")
|
|
||||||
if jobID == "" {
|
|
||||||
response.Error(c, 400, "job_id is required")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
userID := c.GetInt64("userID")
|
|
||||||
starID := c.GetInt64("starID")
|
|
||||||
|
|
||||||
job, err := ctrl.minimaxService.GetJob(c.Request.Context(), jobID, userID, starID)
|
|
||||||
if err != nil {
|
|
||||||
if strings.Contains(err.Error(), "not found") {
|
|
||||||
response.Error(c, 404, "Job not found")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if strings.Contains(err.Error(), "access denied") {
|
|
||||||
response.Error(c, 403, "Access denied")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
response.Error(c, 500, "Failed to get job: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
response.Success(c, &dto.ImageJobResponse{
|
|
||||||
JobID: job.JobID,
|
|
||||||
Status: string(job.Status),
|
|
||||||
Progress: job.Progress,
|
|
||||||
Images: job.Images,
|
|
||||||
ErrorMsg: job.ErrorMsg,
|
|
||||||
CreatedAt: job.CreatedAt,
|
|
||||||
UpdatedAt: job.UpdatedAt,
|
|
||||||
CompletedAt: job.CompletedAt,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -174,7 +174,6 @@ func SetupRouter(userClient *client.Client, socialClient *client.Client, assetCl
|
|||||||
assets.GET("/mints/:order_id", assetCtrl.GetMintOrder) // 查询铸造订单状态
|
assets.GET("/mints/:order_id", assetCtrl.GetMintOrder) // 查询铸造订单状态
|
||||||
assets.DELETE("/mints/:order_id", assetCtrl.CancelMintOrder) // 取消铸造订单
|
assets.DELETE("/mints/:order_id", assetCtrl.CancelMintOrder) // 取消铸造订单
|
||||||
assets.POST("/mints/image/generation", assetCtrl.ImageGeneration) // 图生图
|
assets.POST("/mints/image/generation", assetCtrl.ImageGeneration) // 图生图
|
||||||
assets.GET("/mints/image/generation/:job_id", assetCtrl.GetImageJob) // 查询任务
|
|
||||||
assets.GET("/me/items", assetCtrl.GetMyAssets) // 获取我的藏品列表
|
assets.GET("/me/items", assetCtrl.GetMyAssets) // 获取我的藏品列表
|
||||||
assets.GET("/:asset_id", assetCtrl.GetAsset) // 获取资产详情
|
assets.GET("/:asset_id", assetCtrl.GetAsset) // 获取资产详情
|
||||||
assets.GET("/:asset_id/status", assetCtrl.GetAssetStatus) // 查询上链状态
|
assets.GET("/:asset_id/status", assetCtrl.GetAssetStatus) // 查询上链状态
|
||||||
|
|||||||
@ -6,6 +6,7 @@ require (
|
|||||||
dubbo.apache.org/dubbo-go/v3 v3.3.1
|
dubbo.apache.org/dubbo-go/v3 v3.3.1
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||||
go.uber.org/zap v1.27.1
|
go.uber.org/zap v1.27.1
|
||||||
|
golang.org/x/crypto v0.46.0
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b
|
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b
|
||||||
google.golang.org/protobuf v1.36.11
|
google.golang.org/protobuf v1.36.11
|
||||||
gorm.io/driver/postgres v1.6.0
|
gorm.io/driver/postgres v1.6.0
|
||||||
@ -90,7 +91,6 @@ require (
|
|||||||
go.uber.org/atomic v1.10.0 // indirect
|
go.uber.org/atomic v1.10.0 // indirect
|
||||||
go.uber.org/mock v0.5.0 // indirect
|
go.uber.org/mock v0.5.0 // indirect
|
||||||
go.uber.org/multierr v1.10.0 // indirect
|
go.uber.org/multierr v1.10.0 // indirect
|
||||||
golang.org/x/crypto v0.46.0 // indirect
|
|
||||||
golang.org/x/mod v0.30.0 // indirect
|
golang.org/x/mod v0.30.0 // indirect
|
||||||
golang.org/x/net v0.47.0 // indirect
|
golang.org/x/net v0.47.0 // indirect
|
||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
|
|||||||
@ -59,8 +59,11 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF
|
|||||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||||
github.com/alibaba/sentinel-golang v1.0.4 h1:i0wtMvNVdy7vM4DdzYrlC4r/Mpk1OKUUBurKKkWhEo8=
|
github.com/alibaba/sentinel-golang v1.0.4 h1:i0wtMvNVdy7vM4DdzYrlC4r/Mpk1OKUUBurKKkWhEo8=
|
||||||
github.com/alibaba/sentinel-golang v1.0.4/go.mod h1:Lag5rIYyJiPOylK8Kku2P+a23gdKMMqzQS7wTnjWEpk=
|
github.com/alibaba/sentinel-golang v1.0.4/go.mod h1:Lag5rIYyJiPOylK8Kku2P+a23gdKMMqzQS7wTnjWEpk=
|
||||||
|
github.com/alibabacloud-go/debug v1.0.0/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=
|
||||||
github.com/alibabacloud-go/debug v1.0.1 h1:MsW9SmUtbb1Fnt3ieC6NNZi6aEwrXfDksD4QA6GSbPg=
|
github.com/alibabacloud-go/debug v1.0.1 h1:MsW9SmUtbb1Fnt3ieC6NNZi6aEwrXfDksD4QA6GSbPg=
|
||||||
|
github.com/alibabacloud-go/debug v1.0.1/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=
|
||||||
github.com/alibabacloud-go/tea v1.2.2 h1:aTsR6Rl3ANWPfqeQugPglfurloyBJY85eFy7Gc1+8oU=
|
github.com/alibabacloud-go/tea v1.2.2 h1:aTsR6Rl3ANWPfqeQugPglfurloyBJY85eFy7Gc1+8oU=
|
||||||
|
github.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk=
|
||||||
github.com/alibabacloud-go/tea-utils v1.4.4 h1:lxCDvNCdTo9FaXKKq45+4vGETQUKNOW/qKTcX9Sk53o=
|
github.com/alibabacloud-go/tea-utils v1.4.4 h1:lxCDvNCdTo9FaXKKq45+4vGETQUKNOW/qKTcX9Sk53o=
|
||||||
github.com/alibabacloud-go/tea-utils v1.4.4/go.mod h1:KNcT0oXlZZxOXINnZBs6YvgOd5aYp9U67G+E3R8fcQw=
|
github.com/alibabacloud-go/tea-utils v1.4.4/go.mod h1:KNcT0oXlZZxOXINnZBs6YvgOd5aYp9U67G+E3R8fcQw=
|
||||||
github.com/aliyun/alibaba-cloud-sdk-go v1.61.18/go.mod h1:v8ESoHo4SyHmuB4b1tJqDHxfTGEciD+yhvOU/5s1Rfk=
|
github.com/aliyun/alibaba-cloud-sdk-go v1.61.18/go.mod h1:v8ESoHo4SyHmuB4b1tJqDHxfTGEciD+yhvOU/5s1Rfk=
|
||||||
@ -726,6 +729,7 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
|
|||||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
|
github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
|
||||||
github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||||
@ -803,6 +807,8 @@ golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3
|
|||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||||
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
@ -843,6 +849,8 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB
|
|||||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
@ -889,6 +897,10 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
|
|||||||
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
|
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
|
||||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20211105192438-b53810dc28af/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20211105192438-b53810dc28af/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
|
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
@ -910,6 +922,7 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
@ -976,18 +989,28 @@ golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20211106132015-ebca88c72f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20211106132015-ebca88c72f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
|
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
@ -996,6 +1019,10 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
|||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
@ -1064,6 +1091,8 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f
|
|||||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||||
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||||
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
@ -1201,6 +1230,7 @@ gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
|||||||
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||||
|
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
|
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||||
|
|||||||
@ -3,370 +3,123 @@ package service
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
|
||||||
"image/gif"
|
|
||||||
"image/jpeg"
|
|
||||||
"image/png"
|
|
||||||
"io"
|
"io"
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/nfnt/resize"
|
|
||||||
"github.com/topfans/backend/services/assetService/config"
|
|
||||||
dto "github.com/topfans/backend/gateway/dto"
|
dto "github.com/topfans/backend/gateway/dto"
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// JobStatus 任务状态
|
// ImageGenerationResponse 图生图响应
|
||||||
type JobStatus string
|
type ImageGenerationResponse struct {
|
||||||
|
Images []string `json:"images"`
|
||||||
const (
|
|
||||||
StatusPending JobStatus = "PENDING"
|
|
||||||
StatusProcessing JobStatus = "PROCESSING"
|
|
||||||
StatusCompleted JobStatus = "COMPLETED"
|
|
||||||
StatusFailed JobStatus = "FAILED"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ImageGenerationJob 图生图任务
|
|
||||||
type ImageGenerationJob struct {
|
|
||||||
JobID string `json:"job_id"`
|
|
||||||
UserID int64 `json:"user_id"`
|
|
||||||
StarID int64 `json:"star_id"`
|
|
||||||
Status JobStatus `json:"status"`
|
|
||||||
Progress int `json:"progress"`
|
|
||||||
Images []string `json:"images,omitempty"`
|
|
||||||
ErrorMsg string `json:"error_msg,omitempty"`
|
|
||||||
Request *dto.ImageGenerationRequest `json:"request,omitempty"`
|
|
||||||
CreatedAt int64 `json:"created_at"`
|
|
||||||
UpdatedAt int64 `json:"updated_at"`
|
|
||||||
CompletedAt int64 `json:"completed_at,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MinimaxService MiniMax API 转发服务
|
// MinimaxService MiniMax API 转发服务
|
||||||
type MinimaxService interface {
|
type MinimaxService interface {
|
||||||
CreateJob(ctx context.Context, userID, starID int64, req *dto.ImageGenerationRequest) (*ImageGenerationJob, error)
|
GenerateImage(ctx context.Context, req *dto.ImageGenerationRequest) (*ImageGenerationResponse, error)
|
||||||
GetJob(ctx context.Context, jobID string, userID, starID int64) (*ImageGenerationJob, error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type minimaxService struct {
|
type minimaxService struct {
|
||||||
config *config.AssetConfig
|
|
||||||
jobs map[string]*ImageGenerationJob
|
|
||||||
jobsLock sync.RWMutex
|
|
||||||
client *http.Client
|
client *http.Client
|
||||||
stopCh chan struct{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMinimaxService 创建 MiniMax 服务
|
// NewMinimaxService 创建 MiniMax 服务
|
||||||
func NewMinimaxService(cfg *config.AssetConfig) MinimaxService {
|
func NewMinimaxService() MinimaxService {
|
||||||
svc := &minimaxService{
|
return &minimaxService{
|
||||||
config: cfg,
|
client: &http.Client{Timeout: 320 * time.Second},
|
||||||
jobs: make(map[string]*ImageGenerationJob),
|
|
||||||
client: &http.Client{Timeout: 120 * time.Second},
|
|
||||||
stopCh: make(chan struct{}),
|
|
||||||
}
|
|
||||||
go svc.cleanupExpiredJobs()
|
|
||||||
return svc
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateJob 创建图生图任务
|
|
||||||
func (s *minimaxService) CreateJob(ctx context.Context, userID, starID int64, req *dto.ImageGenerationRequest) (*ImageGenerationJob, error) {
|
|
||||||
jobID := uuid.New().String()
|
|
||||||
now := time.Now().UnixMilli()
|
|
||||||
|
|
||||||
job := &ImageGenerationJob{
|
|
||||||
JobID: jobID,
|
|
||||||
UserID: userID,
|
|
||||||
StarID: starID,
|
|
||||||
Status: StatusProcessing,
|
|
||||||
Progress: 0,
|
|
||||||
Request: req,
|
|
||||||
CreatedAt: now,
|
|
||||||
UpdatedAt: now,
|
|
||||||
}
|
|
||||||
|
|
||||||
s.jobsLock.Lock()
|
|
||||||
s.jobs[jobID] = job
|
|
||||||
s.jobsLock.Unlock()
|
|
||||||
|
|
||||||
go s.processJob(job)
|
|
||||||
|
|
||||||
return job, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetJob 获取任务
|
|
||||||
func (s *minimaxService) GetJob(ctx context.Context, jobID string, userID, starID int64) (*ImageGenerationJob, error) {
|
|
||||||
s.jobsLock.RLock()
|
|
||||||
job, ok := s.jobs[jobID]
|
|
||||||
s.jobsLock.RUnlock()
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("job not found")
|
|
||||||
}
|
|
||||||
if job.UserID != userID || job.StarID != starID {
|
|
||||||
return nil, fmt.Errorf("access denied")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy data to avoid race
|
|
||||||
result := *job
|
|
||||||
result.Images = make([]string, len(job.Images))
|
|
||||||
copy(result.Images, job.Images)
|
|
||||||
return &result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// processJob 异步处理任务
|
|
||||||
func (s *minimaxService) processJob(job *ImageGenerationJob) {
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
job.Status = StatusFailed
|
|
||||||
job.ErrorMsg = fmt.Sprintf("panic: %v", r)
|
|
||||||
job.UpdatedAt = time.Now().UnixMilli()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 1. 校验 SSRF
|
|
||||||
for _, ref := range job.Request.SubjectReference {
|
|
||||||
if err := validateURL(ref.ImageFile); err != nil {
|
|
||||||
job.Status = StatusFailed
|
|
||||||
job.ErrorMsg = "invalid image URL: " + err.Error()
|
|
||||||
job.UpdatedAt = time.Now().UnixMilli()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 压缩图片
|
// GenerateImage 调用 MiniMax 图生图 API(同步调用)
|
||||||
processedRefs := make([]dto.SubjectReference, len(job.Request.SubjectReference))
|
func (s *minimaxService) GenerateImage(ctx context.Context, req *dto.ImageGenerationRequest) (*ImageGenerationResponse, error) {
|
||||||
for i, ref := range job.Request.SubjectReference {
|
|
||||||
job.Progress = 10 + i*20
|
|
||||||
job.UpdatedAt = time.Now().UnixMilli()
|
|
||||||
|
|
||||||
compressed, err := s.compressImageIfNeeded(ref.ImageFile)
|
|
||||||
if err != nil {
|
|
||||||
compressed = ref.ImageFile
|
|
||||||
zap.S().Warnf("Image compression failed, using original: %v", err)
|
|
||||||
}
|
|
||||||
processedRefs[i] = dto.SubjectReference{
|
|
||||||
Type: ref.Type,
|
|
||||||
ImageFile: compressed,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
job.Progress = 50
|
|
||||||
job.UpdatedAt = time.Now().UnixMilli()
|
|
||||||
|
|
||||||
// 3. 调用 MiniMax API
|
|
||||||
images, err := s.callMiniMaxAPI(job.Request.Model, job.Request.Prompt, job.Request.AspectRatio, processedRefs, job.Request.N)
|
|
||||||
if err != nil {
|
|
||||||
job.Status = StatusFailed
|
|
||||||
job.ErrorMsg = "MiniMax API failed: " + err.Error()
|
|
||||||
job.UpdatedAt = time.Now().UnixMilli()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
job.Progress = 90
|
|
||||||
job.UpdatedAt = time.Now().UnixMilli()
|
|
||||||
|
|
||||||
// 4. 完成
|
|
||||||
job.Status = StatusCompleted
|
|
||||||
job.Progress = 100
|
|
||||||
job.Images = images
|
|
||||||
job.CompletedAt = time.Now().UnixMilli()
|
|
||||||
job.UpdatedAt = time.Now().UnixMilli()
|
|
||||||
}
|
|
||||||
|
|
||||||
// callMiniMaxAPI 调用 MiniMax API
|
|
||||||
func (s *minimaxService) callMiniMaxAPI(model, prompt, aspectRatio string, refs []dto.SubjectReference, n int) ([]string, error) {
|
|
||||||
apiURL := os.Getenv("MINIMAX_API_URL")
|
apiURL := os.Getenv("MINIMAX_API_URL")
|
||||||
apiKey := os.Getenv("MINIMAX_API_KEY")
|
apiKey := os.Getenv("MINIMAX_API_KEY")
|
||||||
|
|
||||||
payload := map[string]interface{}{
|
payload := map[string]interface{}{
|
||||||
"model": model,
|
"model": req.Model,
|
||||||
"prompt": prompt,
|
"prompt": req.Prompt,
|
||||||
"aspect_ratio": aspectRatio,
|
"aspect_ratio": req.AspectRatio,
|
||||||
"subject_reference": refs,
|
"subject_reference": req.SubjectReference,
|
||||||
"n": n,
|
"n": req.N,
|
||||||
|
"response_format": "base64",
|
||||||
}
|
}
|
||||||
|
|
||||||
jsonData, err := json.Marshal(payload)
|
jsonData, err := json.Marshal(payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(jsonData))
|
httpReq, err := http.NewRequestWithContext(ctx, "POST", apiURL, bytes.NewBuffer(jsonData))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||||
}
|
}
|
||||||
req.Header.Set("Authorization", "Bearer "+apiKey)
|
httpReq.Header.Set("Authorization", "Bearer "+apiKey)
|
||||||
req.Header.Set("Content-Type", "application/json")
|
httpReq.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
resp, err := s.client.Do(req)
|
resp, err := s.client.Do(httpReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("failed to call MiniMax API: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read response body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
body, _ := io.ReadAll(resp.Body)
|
return nil, fmt.Errorf("MiniMax API returned status %d: %s", resp.StatusCode, string(body))
|
||||||
return nil, fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(body))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var result struct {
|
var rawResp map[string]interface{}
|
||||||
Images []struct {
|
if err := json.Unmarshal(body, &rawResp); err != nil {
|
||||||
URL string `json:"url"`
|
return nil, fmt.Errorf("failed to parse response: %w", err)
|
||||||
} `json:"images"`
|
|
||||||
}
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
images := make([]string, len(result.Images))
|
var images []string
|
||||||
for i, img := range result.Images {
|
|
||||||
images[i] = img.URL
|
// MiniMax API 响应格式:
|
||||||
}
|
// {"id": "...", "data": {"image_base64": ["..."]}, "base_resp": {"status_code": 0, "status_msg": "success"}}
|
||||||
return images, nil
|
data, ok := rawResp["data"].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("invalid MiniMax response format: missing data field: %s", string(body))
|
||||||
}
|
}
|
||||||
|
|
||||||
// compressImageIfNeeded 下载并压缩图片
|
// 检查 base_resp 状态码
|
||||||
func (s *minimaxService) compressImageIfNeeded(imageURL string) (string, error) {
|
if baseResp, ok := rawResp["base_resp"].(map[string]interface{}); ok {
|
||||||
client := &http.Client{Timeout: 30 * time.Second}
|
if statusCode, ok := baseResp["status_code"].(float64); ok && statusCode != 0 {
|
||||||
resp, err := client.Get(imageURL)
|
statusMsg, _ := baseResp["status_msg"].(string)
|
||||||
if err != nil {
|
return nil, fmt.Errorf("MiniMax API error: code=%d, msg=%s", int(statusCode), statusMsg)
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
imgData, err := io.ReadAll(io.LimitReader(resp.Body, 10*1024*1024)) // 10MB limit
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
// First decode config to check dimensions (prevents decompression bomb)
|
|
||||||
cfg, format, err := image.DecodeConfig(bytes.NewReader(imgData))
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
// Limit decoded image to reasonable size (e.g., 4096x4096 = 64M pixels)
|
|
||||||
if int64(cfg.Width)*int64(cfg.Height) > 4096*4096 {
|
|
||||||
return "", fmt.Errorf("image too large: %dx%d", cfg.Width, cfg.Height)
|
|
||||||
}
|
|
||||||
// Full decode (already limited by size)
|
|
||||||
img, _, err := image.Decode(bytes.NewReader(imgData))
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
_ = format // suppress unused warning
|
|
||||||
|
|
||||||
bounds := img.Bounds()
|
|
||||||
maxDim := uint(1024)
|
|
||||||
newWidth := uint(bounds.Dx())
|
|
||||||
newHeight := uint(bounds.Dy())
|
|
||||||
|
|
||||||
if newWidth > maxDim || newHeight > maxDim {
|
|
||||||
if newWidth > newHeight {
|
|
||||||
ratio := float64(maxDim) / float64(newWidth)
|
|
||||||
newWidth = maxDim
|
|
||||||
newHeight = uint(float64(newHeight) * ratio)
|
|
||||||
} else {
|
|
||||||
ratio := float64(maxDim) / float64(newHeight)
|
|
||||||
newHeight = maxDim
|
|
||||||
newWidth = uint(float64(newWidth) * ratio)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if newWidth == uint(bounds.Dx()) && newHeight == uint(bounds.Dy()) {
|
// 优先使用 base64 格式(当 response_format=base64 时)
|
||||||
return "data:image/jpeg;base64," + base64.StdEncoding.EncodeToString(imgData), nil
|
if base64Imgs, ok := data["image_base64"].([]interface{}); ok {
|
||||||
}
|
for _, img := range base64Imgs {
|
||||||
|
if base64Str, ok := img.(string); ok && base64Str != "" {
|
||||||
resized := resize.Thumbnail(newWidth, newHeight, img, resize.Lanczos3)
|
images = append(images, "data:image/jpeg;base64,"+base64Str)
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
switch format {
|
|
||||||
case "png":
|
|
||||||
err = png.Encode(&buf, resized)
|
|
||||||
case "gif":
|
|
||||||
err = gif.Encode(&buf, resized, nil)
|
|
||||||
default:
|
|
||||||
err = jpeg.Encode(&buf, resized, &jpeg.Options{Quality: 85})
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
encoded := base64.StdEncoding.EncodeToString(buf.Bytes())
|
|
||||||
mimeType := "image/jpeg"
|
|
||||||
if format == "png" {
|
|
||||||
mimeType = "image/png"
|
|
||||||
} else if format == "gif" {
|
|
||||||
mimeType = "image/gif"
|
|
||||||
}
|
|
||||||
return "data:" + mimeType + ";base64," + encoded, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// validateURL 校验 URL 防止 SSRF
|
|
||||||
func validateURL(rawURL string) error {
|
|
||||||
if rawURL == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
u, err := url.Parse(rawURL)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if u.Scheme != "http" && u.Scheme != "https" {
|
|
||||||
return fmt.Errorf("unsupported scheme: %s", u.Scheme)
|
|
||||||
}
|
|
||||||
host := u.Hostname()
|
|
||||||
|
|
||||||
ip := net.ParseIP(host)
|
|
||||||
if ip != nil {
|
|
||||||
if ip.IsLoopback() || ip.IsPrivate() || ip.IsUnspecified() {
|
|
||||||
return fmt.Errorf("private IP not allowed: %s", host)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
lowerHost := strings.ToLower(host)
|
|
||||||
if strings.HasSuffix(lowerHost, ".local") ||
|
|
||||||
strings.HasSuffix(lowerHost, ".internal") ||
|
|
||||||
strings.HasSuffix(lowerHost, ".private") {
|
|
||||||
return fmt.Errorf("internal domain not allowed: %s", host)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// cleanupExpiredJobs 清理过期任务
|
|
||||||
func (s *minimaxService) cleanupExpiredJobs() {
|
|
||||||
ticker := time.NewTicker(1 * time.Hour)
|
|
||||||
defer ticker.Stop()
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-s.stopCh:
|
|
||||||
return
|
|
||||||
case <-ticker.C:
|
|
||||||
s.jobsLock.Lock()
|
|
||||||
now := time.Now().UnixMilli()
|
|
||||||
expiredThreshold := int64(24 * 60 * 60 * 1000) // 24h
|
|
||||||
for jobID, job := range s.jobs {
|
|
||||||
if job.Status == StatusCompleted || job.Status == StatusFailed {
|
|
||||||
if now-job.UpdatedAt > expiredThreshold {
|
|
||||||
delete(s.jobs, jobID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s.jobsLock.Unlock()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close 关闭服务
|
// 回退使用 image_urls 格式
|
||||||
func (s *minimaxService) Close() {
|
if len(images) == 0 {
|
||||||
close(s.stopCh)
|
if urlImgs, ok := data["image_urls"].([]interface{}); ok {
|
||||||
|
for _, img := range urlImgs {
|
||||||
|
if urlStr, ok := img.(string); ok && urlStr != "" {
|
||||||
|
images = append(images, urlStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(images) == 0 {
|
||||||
|
return nil, fmt.Errorf("no images found in MiniMax response: %s", string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ImageGenerationResponse{Images: images}, nil
|
||||||
}
|
}
|
||||||
382
docs/MiniMax 图生图 API 集成方案.md
Normal file
382
docs/MiniMax 图生图 API 集成方案.md
Normal file
@ -0,0 +1,382 @@
|
|||||||
|
# MiniMax 图生图 API 集成方案
|
||||||
|
|
||||||
|
## 一、需求概述
|
||||||
|
|
||||||
|
前端传递参数 → 后端调用 MiniMax 图生图 API → 后端返回结果给前端
|
||||||
|
|
||||||
|
## 二、现有项目架构
|
||||||
|
|
||||||
|
| 层级 | 技术栈 | 说明 |
|
||||||
|
|------|--------|------|
|
||||||
|
| 前端 | uni-app (Vue 3) | 使用 `uni.request` 发起请求 |
|
||||||
|
| 后端 | Go + Gin | API Gateway 模式,端口 8080 |
|
||||||
|
| 配置 | `.env` 文件 | API keys 等敏感配置 |
|
||||||
|
|
||||||
|
## 三、MiniMax API 信息
|
||||||
|
|
||||||
|
**接口地址**: `POST https://api.minimaxi.com/v1/image_generation`
|
||||||
|
|
||||||
|
**请求头**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Authorization": "Bearer <token>",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**请求体**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"model": "image-01",
|
||||||
|
"prompt": "描述文本",
|
||||||
|
"aspect_ratio": "16:9",
|
||||||
|
"subject_reference": [
|
||||||
|
{
|
||||||
|
"type": "character",
|
||||||
|
"image_file": "https://..."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"n": 2
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 四、后端设计方案
|
||||||
|
|
||||||
|
### 4.1 配置文件新增 (.env)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# MiniMax API 配置
|
||||||
|
MINIMAX_API_KEY=your_api_key_here
|
||||||
|
MINIMAX_API_URL=https://api.minimaxi.com/v1/image_generation
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 新增 DTO (gateway/dto/image_dto.go)
|
||||||
|
|
||||||
|
```go
|
||||||
|
// MiniMax 图生图请求
|
||||||
|
type ImageGenerationRequest struct {
|
||||||
|
Model string `json:"model" binding:"required"`
|
||||||
|
Prompt string `json:"prompt" binding:"required"`
|
||||||
|
AspectRatio string `json:"aspect_ratio"`
|
||||||
|
SubjectReference []SubjectReference `json:"subject_reference"`
|
||||||
|
N int `json:"n"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubjectReference struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
ImageFile string `json:"image_file"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MiniMax 图生图响应
|
||||||
|
type ImageGenerationResponse struct {
|
||||||
|
Model string `json:"model"`
|
||||||
|
Prompt string `json:"prompt"`
|
||||||
|
AspectRatio string `json:"aspect_ratio"`
|
||||||
|
Images []Image `json:"images"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Image struct {
|
||||||
|
URL string `json:"url"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 新增 Controller (gateway/controller/image_controller.go)
|
||||||
|
|
||||||
|
```go
|
||||||
|
// ImageGeneration 图生图 - 调用 MiniMax API
|
||||||
|
// @Summary 图生图
|
||||||
|
// @Description 前端传递参数,后端调用 MiniMax 图生图 API
|
||||||
|
// @Tags assets
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param request body dto.ImageGenerationRequest true "图生图请求"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Router /api/v1/assets/mints/image/generation [post]
|
||||||
|
func (ctrl *AssetController) ImageGeneration(c *gin.Context) {
|
||||||
|
var req dto.ImageGenerationRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.Error(c, 400, "Invalid request parameters: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从 context 获取 userID 和 starID(由认证中间件设置)
|
||||||
|
userID := c.GetInt64("userID")
|
||||||
|
starID := c.GetInt64("starID")
|
||||||
|
|
||||||
|
// 调用资产服务的 MiniMax 转发服务
|
||||||
|
result, err := ctrl.assetService.CallMiniMaxImageAPI(c.Request.Context(), userID, starID, &req)
|
||||||
|
if err != nil {
|
||||||
|
response.Error(c, 500, "Image generation failed: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Success(c, result)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 新增 Service (assetService/service/minimax_service.go)
|
||||||
|
|
||||||
|
```go
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/gif"
|
||||||
|
"image/jpeg"
|
||||||
|
"image/png"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/nfnt/resize"
|
||||||
|
"github.com/topfans/backend/services/assetService/config"
|
||||||
|
dto "github.com/topfans/backend/gateway/dto"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MinimaxService MiniMax API 转发服务接口
|
||||||
|
type MinimaxService interface {
|
||||||
|
// CallMiniMaxImageAPI 调用 MiniMax 图生图 API
|
||||||
|
CallMiniMaxImageAPI(ctx context.Context, userID, starID int64, req *dto.ImageGenerationRequest) (*dto.ImageGenerationResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// minimaxService MiniMax API 转发服务实现
|
||||||
|
type minimaxService struct {
|
||||||
|
config *config.AssetConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMinimaxService 创建 MiniMax 服务实例
|
||||||
|
func NewMinimaxService(cfg *config.AssetConfig) MinimaxService {
|
||||||
|
return &minimaxService{config: cfg}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CallMiniMaxImageAPI 调用 MiniMax 图生图 API
|
||||||
|
func (s *minimaxService) CallMiniMaxImageAPI(ctx context.Context, userID, starID int64, req *dto.ImageGenerationRequest) (*dto.ImageGenerationResponse, error) {
|
||||||
|
// 1. 压缩 subject_reference 中的图片
|
||||||
|
processedRefs := make([]dto.SubjectReference, len(req.SubjectReference))
|
||||||
|
for i, ref := range req.SubjectReference {
|
||||||
|
compressedURL, err := s.compressImageIfNeeded(ref.ImageFile)
|
||||||
|
if err != nil {
|
||||||
|
// 压缩失败时使用原图
|
||||||
|
compressedURL = ref.ImageFile
|
||||||
|
zap.S().Warnf("Image compression failed, using original: %v", err)
|
||||||
|
}
|
||||||
|
processedRefs[i] = dto.SubjectReference{
|
||||||
|
Type: ref.Type,
|
||||||
|
ImageFile: compressedURL,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 构建请求体
|
||||||
|
payload := map[string]interface{}{
|
||||||
|
"model": req.Model,
|
||||||
|
"prompt": req.Prompt,
|
||||||
|
"aspect_ratio": req.AspectRatio,
|
||||||
|
"subject_reference": processedRefs,
|
||||||
|
"n": req.N,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 发送 HTTP POST 请求到 MiniMax
|
||||||
|
apiURL := s.config.GetMiniMaxAPIURL()
|
||||||
|
apiKey := s.config.GetMiniMaxAPIKey()
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: 120 * time.Second}
|
||||||
|
jsonData, _ := json.Marshal(payload)
|
||||||
|
|
||||||
|
httpReq, err := http.NewRequestWithContext(ctx, "POST", apiURL, bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
httpReq.Header.Set("Authorization", "Bearer "+apiKey)
|
||||||
|
httpReq.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, err := client.Do(httpReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to call MiniMax API: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// 4. 解析响应
|
||||||
|
var result dto.ImageGenerationResponse
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// compressImageIfNeeded 下载图片、压缩后返回 base64 编码
|
||||||
|
func (s *minimaxService) compressImageIfNeeded(imageURL string) (string, error) {
|
||||||
|
// 下载图片
|
||||||
|
resp, err := http.Get(imageURL)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// 读取图片数据
|
||||||
|
imgData, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解码图片(自动识别格式)
|
||||||
|
img, format, err := image.Decode(bytes.NewReader(imgData))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算压缩后的尺寸(最大边 1024px,保持宽高比)
|
||||||
|
bounds := img.Bounds()
|
||||||
|
maxDim := uint(1024)
|
||||||
|
newWidth := uint(bounds.Dx())
|
||||||
|
newHeight := uint(bounds.Dy())
|
||||||
|
|
||||||
|
if newWidth > maxDim || newHeight > maxDim {
|
||||||
|
if newWidth > newHeight {
|
||||||
|
ratio := float64(maxDim) / float64(newWidth)
|
||||||
|
newWidth = maxDim
|
||||||
|
newHeight = uint(float64(newHeight) * ratio)
|
||||||
|
} else {
|
||||||
|
ratio := float64(maxDim) / float64(newHeight)
|
||||||
|
newHeight = maxDim
|
||||||
|
newWidth = uint(float64(newWidth) * ratio)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果图片尺寸没变,不压缩直接返回原图
|
||||||
|
if newWidth == uint(bounds.Dx()) && newHeight == uint(bounds.Dy()) {
|
||||||
|
return "data:image/jpeg;base64," + base64.StdEncoding.EncodeToString(imgData), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缩放图片(Lanczos 算法,质量好)
|
||||||
|
resized := resize.Thumbnail(newWidth, newHeight, img, resize.Lanczos)
|
||||||
|
|
||||||
|
// 重新编码为 JPEG(质量 85%,体积小)
|
||||||
|
var buf bytes.Buffer
|
||||||
|
switch format {
|
||||||
|
case "png":
|
||||||
|
err = png.Encode(&buf, resized)
|
||||||
|
case "gif":
|
||||||
|
err = gif.Encode(&buf, resized, nil)
|
||||||
|
default:
|
||||||
|
err = jpeg.Encode(&buf, resized, &jpeg.Options{Quality: 85})
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回 base64 编码(data URI 格式)
|
||||||
|
encoded := base64.StdEncoding.EncodeToString(buf.Bytes())
|
||||||
|
mimeType := "image/jpeg"
|
||||||
|
if format == "png" {
|
||||||
|
mimeType = "image/png"
|
||||||
|
} else if format == "gif" {
|
||||||
|
mimeType = "image/gif"
|
||||||
|
}
|
||||||
|
return "data:" + mimeType + ";base64," + encoded, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.5 路由注册 (gateway/router/router.go)
|
||||||
|
|
||||||
|
在 assets 路由组内添加:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 资产相关路由(需要认证)
|
||||||
|
assets := v1.Group("/assets")
|
||||||
|
assets.Use(middleware.AuthMiddleware())
|
||||||
|
{
|
||||||
|
// ... 现有路由 ...
|
||||||
|
assets.POST("/mints/image/generation", assetCtrl.ImageGeneration) // 图生图(MiniMax API)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 五、前端调用方案
|
||||||
|
|
||||||
|
**架构说明**:前端只与后端 API 通信,不直接调用 MiniMax。后端作为代理调用 MiniMax。
|
||||||
|
|
||||||
|
### 5.1 前端 API (frontend/utils/api.js) - 已存在
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 图生图 API - 前端传递必要参数,后端调用 MiniMax
|
||||||
|
export function imageGenerationApi(params) {
|
||||||
|
return request({
|
||||||
|
url: '/api/v1/assets/mints/image/generation',
|
||||||
|
method: 'POST',
|
||||||
|
data: params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 前端调用示例
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 前端只需要传递这些参数,具体的 MiniMax API 调用由后端完成
|
||||||
|
imageGenerationApi({
|
||||||
|
prompt: '描述文字',
|
||||||
|
aspect_ratio: '16:9',
|
||||||
|
subject_reference: [
|
||||||
|
{
|
||||||
|
type: 'character',
|
||||||
|
image_file: '用户选择的图片URL'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
n: 2
|
||||||
|
}).then(res => {
|
||||||
|
// res.data.images 是 MiniMax 返回的图片 URL 列表
|
||||||
|
console.log('生成的图片:', res.data.images)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 数据流向
|
||||||
|
|
||||||
|
```
|
||||||
|
前端参数 {prompt, aspect_ratio, subject_reference, n}
|
||||||
|
↓ POST /api/v1/assets/mints/image/generation
|
||||||
|
后端接收参数 → 调用 assetService 的 MiniMax 转发服务 → 调用 MiniMax API
|
||||||
|
↓
|
||||||
|
后端获取响应 → 直接透传 images 数组 → 返回给前端
|
||||||
|
↓
|
||||||
|
前端收到 {code: 200, data: {images: [...]}}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 六、文件修改清单
|
||||||
|
|
||||||
|
| 操作 | 文件路径 | 说明 |
|
||||||
|
|------|----------|------|
|
||||||
|
| 修改 | `backend/.env` | 添加 MiniMax 配置 |
|
||||||
|
| 新增 | `backend/gateway/dto/image_dto.go` | 请求/响应 DTO |
|
||||||
|
| 新增 | `backend/services/assetService/service/minimax_service.go` | MiniMax API 转发 + 图片压缩 |
|
||||||
|
| 新增 | `backend/services/assetService/config/minimax_config.go` | MiniMax 配置读取 |
|
||||||
|
| 修改 | `backend/gateway/controller/asset_controller.go` | 新增 ImageGeneration 处理器 |
|
||||||
|
| 修改 | `backend/gateway/router/router.go` | 注册 `/mints/image/generation` 路由 |
|
||||||
|
| 修改 | `frontend/utils/api.js` | 已存在,无需修改 |
|
||||||
|
|
||||||
|
### 依赖安装
|
||||||
|
```bash
|
||||||
|
cd backend/services/assetService && go get github.com/nfnt/resize
|
||||||
|
```
|
||||||
|
|
||||||
|
## 七、验证方案
|
||||||
|
|
||||||
|
1. **后端启动**: 启动 `backend/gateway/main.go` 和 `backend/services/assetService/main.go`
|
||||||
|
2. **接口测试**: 使用 curl 或 Postman 测试接口
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8080/api/v1/assets/mints/image/generation \
|
||||||
|
-H "Authorization: Bearer <token>" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"model":"image-01","prompt":"test","aspect_ratio":"16:9","n":1,"subject_reference":[]}'
|
||||||
|
```
|
||||||
|
3. **前端测试**: 在页面中调用 `imageGenerationApi()` 并展示返回的图片
|
||||||
|
|
||||||
|
## 八、已确认事项
|
||||||
|
|
||||||
|
- 图片直接返回 MiniMax 的 URL,不经过 OSS
|
||||||
|
- subject_reference.image_file 由前端自定义传递给后端(用户选择的自定义图片)
|
||||||
|
- 转发服务位于 `assetService` 中,不在 Gateway
|
||||||
@ -91,107 +91,32 @@ const revertProgress = () => {
|
|||||||
}, 100);
|
}, 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
// job_id 轮询
|
// 调用图生图API - 同步模式
|
||||||
let jobId = null;
|
|
||||||
|
|
||||||
// 调用图生图API - 异步模式
|
|
||||||
const callImageGeneration = async () => {
|
const callImageGeneration = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await imageGenerationApi(generationData);
|
const res = await imageGenerationApi(generationData);
|
||||||
|
|
||||||
if (res.data && res.data.job_id) {
|
if (res.data && res.data.images && res.data.images.length > 0) {
|
||||||
jobId = res.data.job_id;
|
// 保存生成的图片
|
||||||
console.log('[GenerationLoading] 任务已创建:', jobId);
|
uni.setStorageSync('generated_images', JSON.stringify(res.data.images));
|
||||||
// 开始轮询
|
|
||||||
pollJobStatus();
|
|
||||||
} else {
|
|
||||||
uni.showToast({
|
|
||||||
title: '创建任务失败',
|
|
||||||
icon: 'none',
|
|
||||||
duration: 2000
|
|
||||||
});
|
|
||||||
revertProgress();
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[GenerationLoading] 创建任务失败:', err);
|
|
||||||
uni.showToast({
|
|
||||||
title: '创建任务失败',
|
|
||||||
icon: 'none',
|
|
||||||
duration: 2000
|
|
||||||
});
|
|
||||||
revertProgress();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 轮询任务状态
|
|
||||||
const pollJobStatus = () => {
|
|
||||||
let pollCount = 0;
|
|
||||||
const maxPolls = 40; // 120秒 / 3秒 = 40次
|
|
||||||
|
|
||||||
const poll = async () => {
|
|
||||||
if (!jobId) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await uni.request({
|
|
||||||
url: 'http://192.168.110.60:8080' + `/api/v1/assets/mints/image/generation/${jobId}`,
|
|
||||||
method: 'GET',
|
|
||||||
header: {
|
|
||||||
'Authorization': `Bearer ${uni.getStorageSync('access_token')}`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = res.data?.data;
|
|
||||||
if (data) {
|
|
||||||
// 更新进度
|
|
||||||
if (data.progress) {
|
|
||||||
progress.value = Math.min(data.progress, 90);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.status === 'COMPLETED') {
|
|
||||||
// 完成
|
|
||||||
const imageUrls = data.images || [];
|
|
||||||
if (imageUrls.length > 0) {
|
|
||||||
uni.setStorageSync('generated_images', JSON.stringify(imageUrls));
|
|
||||||
completeProgress();
|
completeProgress();
|
||||||
} else {
|
} else {
|
||||||
uni.showToast({ title: '未生成图片', icon: 'none' });
|
|
||||||
revertProgress();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
} else if (data.status === 'FAILED') {
|
|
||||||
// 失败
|
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: data.error_msg || '生成失败',
|
title: '未生成图片',
|
||||||
icon: 'none',
|
icon: 'none',
|
||||||
duration: 2000
|
duration: 2000
|
||||||
});
|
});
|
||||||
revertProgress();
|
revertProgress();
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
pollCount++;
|
|
||||||
if (pollCount >= maxPolls) {
|
|
||||||
uni.showToast({ title: '生成超时,请重试', icon: 'none' });
|
|
||||||
revertProgress();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3秒后继续轮询
|
|
||||||
setTimeout(poll, 3000);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[GenerationLoading] 轮询失败:', err);
|
console.error('[GenerationLoading] 生成失败:', err);
|
||||||
pollCount++;
|
uni.showToast({
|
||||||
if (pollCount >= maxPolls) {
|
title: err.message || '生成失败',
|
||||||
uni.showToast({ title: '查询失败,请重试', icon: 'none' });
|
icon: 'none',
|
||||||
|
duration: 2000
|
||||||
|
});
|
||||||
revertProgress();
|
revertProgress();
|
||||||
} else {
|
|
||||||
setTimeout(poll, 3000);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
poll();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理成功
|
// 处理成功
|
||||||
|
|||||||
@ -456,6 +456,7 @@ export function imageGenerationApi(params) {
|
|||||||
data: params
|
data: params
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取热度排行榜
|
// 获取热度排行榜
|
||||||
export function getHotRankingApi(dimension = 'total', starId = null, page = 1, pageSize = 10) {
|
export function getHotRankingApi(dimension = 'total', starId = null, page = 1, pageSize = 10) {
|
||||||
let url = `/api/v1/rankings/hot?dimension=${dimension}&page=${page}&page_size=${pageSize}`
|
let url = `/api/v1/rankings/hot?dimension=${dimension}&page=${page}&page_size=${pageSize}`
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user