add user lock for chat api, Prevent insufficient deduction of user power caused by submitting multiple requests at one time
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
- Bug 修复:修复超级管理员无法修改密码的 Bug
|
- Bug 修复:修复超级管理员无法修改密码的 Bug
|
||||||
- Bug 修复:微信登录配置更新后,没有同步更新到系统配置
|
- Bug 修复:微信登录配置更新后,没有同步更新到系统配置
|
||||||
|
- 功能优化: 给 AI 对话 API 加上线程锁,确保同一个用户同时只有一个对话请求
|
||||||
|
|
||||||
## v4.2.6
|
## v4.2.6
|
||||||
|
|
||||||
|
|||||||
@@ -14,3 +14,50 @@ type JimengPower struct {
|
|||||||
VirtualHuman int `json:"virtual_human"` // 数字人视频生成算力,单位:积分/秒
|
VirtualHuman int `json:"virtual_human"` // 数字人视频生成算力,单位:积分/秒
|
||||||
ActionTransfer int `json:"action_transfer"` // 视频动作迁移算力,单位:积分/秒
|
ActionTransfer int `json:"action_transfer"` // 视频动作迁移算力,单位:积分/秒
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JMTaskStatus 任务状态
|
||||||
|
type JMTaskStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
JMTaskStatusInQueue = JMTaskStatus("in_queue") // 任务已提交
|
||||||
|
JMTaskStatusGenerating = JMTaskStatus("generating") // 任务处理中
|
||||||
|
JMTaskStatusDone = JMTaskStatus("done") // 处理完成
|
||||||
|
JMTaskStatusNotFound = JMTaskStatus("not_found") // 任务未找到
|
||||||
|
JMTaskStatusSuccess = JMTaskStatus("success") // 任务成功
|
||||||
|
JMTaskStatusFailed = JMTaskStatus("failed") // 任务失败
|
||||||
|
JMTaskStatusExpired = JMTaskStatus("expired") // 任务过期
|
||||||
|
)
|
||||||
|
|
||||||
|
// JMTaskType 任务类型
|
||||||
|
type JMTaskType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
JMTaskTypeImage = JMTaskType("image") // 文生图
|
||||||
|
JMTaskTypeVideo = JMTaskType("video") // 图生图
|
||||||
|
JMTaskTypeVirtualHuman = JMTaskType("virtual_human") // 图像编辑
|
||||||
|
JMTaskTypeActionTransfer = JMTaskType("action_transfer") // 图像特效
|
||||||
|
)
|
||||||
|
|
||||||
|
// JimengTaskRequest 即梦AI任务请求
|
||||||
|
type JimengTaskRequest struct {
|
||||||
|
ReqKey string `json:"req_key"` // 请求Key
|
||||||
|
// 公共参数
|
||||||
|
Prompt string `json:"prompt,omitempty"`
|
||||||
|
ImageUrls []string `json:"image_urls,omitempty"`
|
||||||
|
|
||||||
|
// 图片生成参数
|
||||||
|
Size string `json:"size,omitempty"`
|
||||||
|
UsePreLLM bool `json:"use_pre_llm,omitempty"`
|
||||||
|
|
||||||
|
// 视频生成参数
|
||||||
|
Duration string `json:"duration,omitempty"` // 视频时长
|
||||||
|
TemplateId string `json:"template_id,omitempty"` // 运镜模板ID
|
||||||
|
AspectRatio string `json:"aspect_ratio,omitempty"`
|
||||||
|
CameraStrength string `json:"camera_strength,omitempty"` // 运镜强度
|
||||||
|
|
||||||
|
// 数字人视频生成参数
|
||||||
|
AudioURL string `json:"audio_url,omitempty"` // 音频URL
|
||||||
|
|
||||||
|
// 视频动作迁移参数
|
||||||
|
VideoURL string `json:"video_url,omitempty"` // 动作视频URL
|
||||||
|
}
|
||||||
|
|||||||
45
api/core/types/user_lock.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||||
|
// * Use of this source code is governed by a Apache-2.0 license
|
||||||
|
// * that can be found in the LICENSE file.
|
||||||
|
// * @Author yangjian102621@163.com
|
||||||
|
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
|
||||||
|
import "sync"
|
||||||
|
|
||||||
|
// UserLockManager 提供基于用户ID的TryLock功能,确保同一用户并发请求串行化
|
||||||
|
type UserLockManager struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
locks map[uint]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserLockManager() *UserLockManager {
|
||||||
|
return &UserLockManager{mu: sync.Mutex{}, locks: make(map[uint]bool)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TryLock 尝试为指定用户加锁。若已被占用返回 false
|
||||||
|
func (m *UserLockManager) TryLock(userId uint) bool {
|
||||||
|
if userId == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
if m.locks[userId] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
m.locks[userId] = true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unlock 释放指定用户的锁
|
||||||
|
func (m *UserLockManager) Unlock(userId uint) {
|
||||||
|
if userId == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.mu.Lock()
|
||||||
|
delete(m.locks, userId)
|
||||||
|
m.mu.Unlock()
|
||||||
|
}
|
||||||
@@ -131,7 +131,7 @@ func (h *AdminJimengHandler) BatchRemove(c *gin.Context) {
|
|||||||
continue // 跳过不存在的
|
continue // 跳过不存在的
|
||||||
}
|
}
|
||||||
tx := h.DB.Begin()
|
tx := h.DB.Begin()
|
||||||
if job.Status != model.JMTaskStatusSuccess && job.Power > 0 {
|
if job.Status != types.JMTaskStatusSuccess && job.Power > 0 {
|
||||||
remark := fmt.Sprintf("任务未成功,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg)
|
remark := fmt.Sprintf("任务未成功,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg)
|
||||||
err = h.userService.IncreasePower(job.UserId, job.Power, model.PowerLog{
|
err = h.userService.IncreasePower(job.UserId, job.Power, model.PowerLog{
|
||||||
Type: types.PowerRefund,
|
Type: types.PowerRefund,
|
||||||
@@ -172,7 +172,7 @@ func (h *AdminJimengHandler) BatchRemove(c *gin.Context) {
|
|||||||
// Stats 获取统计信息
|
// Stats 获取统计信息
|
||||||
func (h *AdminJimengHandler) Stats(c *gin.Context) {
|
func (h *AdminJimengHandler) Stats(c *gin.Context) {
|
||||||
type StatResult struct {
|
type StatResult struct {
|
||||||
Status model.JMTaskStatus `json:"status"`
|
Status types.JMTaskStatus `json:"status"`
|
||||||
Count int64 `json:"count"`
|
Count int64 `json:"count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,13 +198,13 @@ func (h *AdminJimengHandler) Stats(c *gin.Context) {
|
|||||||
for _, stat := range stats {
|
for _, stat := range stats {
|
||||||
result["totalTasks"] = result["totalTasks"].(int64) + stat.Count
|
result["totalTasks"] = result["totalTasks"].(int64) + stat.Count
|
||||||
switch stat.Status {
|
switch stat.Status {
|
||||||
case model.JMTaskStatusInQueue:
|
case types.JMTaskStatusInQueue:
|
||||||
result["pendingTasks"] = stat.Count
|
result["pendingTasks"] = stat.Count
|
||||||
case model.JMTaskStatusSuccess:
|
case types.JMTaskStatusSuccess:
|
||||||
result["completedTasks"] = stat.Count
|
result["completedTasks"] = stat.Count
|
||||||
case model.JMTaskStatusGenerating:
|
case types.JMTaskStatusGenerating:
|
||||||
result["processingTasks"] = stat.Count
|
result["processingTasks"] = stat.Count
|
||||||
case model.JMTaskStatusFailed:
|
case types.JMTaskStatusFailed:
|
||||||
result["failedTasks"] = stat.Count
|
result["failedTasks"] = stat.Count
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ type ChatHandler struct {
|
|||||||
ReqCancelFunc *types.LMap[string, context.CancelFunc] // HttpClient 请求取消 handle function
|
ReqCancelFunc *types.LMap[string, context.CancelFunc] // HttpClient 请求取消 handle function
|
||||||
userService *service.UserService
|
userService *service.UserService
|
||||||
moderationManager *moderation.ServiceManager
|
moderationManager *moderation.ServiceManager
|
||||||
|
userLocks *types.UserLockManager
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewChatHandler(app *core.AppServer, db *gorm.DB, redis *redis.Client, manager *oss.UploaderManager, licenseService *service.LicenseService, userService *service.UserService, moderationManager *moderation.ServiceManager) *ChatHandler {
|
func NewChatHandler(app *core.AppServer, db *gorm.DB, redis *redis.Client, manager *oss.UploaderManager, licenseService *service.LicenseService, userService *service.UserService, moderationManager *moderation.ServiceManager) *ChatHandler {
|
||||||
@@ -80,6 +81,7 @@ func NewChatHandler(app *core.AppServer, db *gorm.DB, redis *redis.Client, manag
|
|||||||
ReqCancelFunc: types.NewLMap[string, context.CancelFunc](),
|
ReqCancelFunc: types.NewLMap[string, context.CancelFunc](),
|
||||||
userService: userService,
|
userService: userService,
|
||||||
moderationManager: moderationManager,
|
moderationManager: moderationManager,
|
||||||
|
userLocks: types.NewUserLockManager(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,6 +122,14 @@ func (h *ChatHandler) Chat(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 用户级并发锁,确保同一用户同时只有一个对话请求
|
||||||
|
if !h.userLocks.TryLock(input.UserId) {
|
||||||
|
pushMessage(c, ChatEventError, "您有一个对话请求正在进行中,请稍后再试或先停止当前生成!")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer h.userLocks.Unlock(input.UserId)
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(c.Request.Context())
|
ctx, cancel := context.WithCancel(c.Request.Context())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
|
|||||||
@@ -50,37 +50,16 @@ func (h *JimengHandler) RegisterRoutes() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// JimengTaskRequest 统一任务请求结构体
|
|
||||||
// 支持所有生图和生成视频类型
|
|
||||||
type JimengTaskRequest struct {
|
|
||||||
TaskType string `json:"task_type" binding:"required"`
|
|
||||||
Prompt string `json:"prompt"`
|
|
||||||
ImageInput string `json:"image_input"`
|
|
||||||
ImageUrls []string `json:"image_urls"`
|
|
||||||
BinaryDataBase64 []string `json:"binary_data_base64"`
|
|
||||||
Scale float64 `json:"scale"`
|
|
||||||
Width int `json:"width"`
|
|
||||||
Height int `json:"height"`
|
|
||||||
Gpen float64 `json:"gpen"`
|
|
||||||
Skin float64 `json:"skin"`
|
|
||||||
SkinUnifi float64 `json:"skin_unifi"`
|
|
||||||
GenMode string `json:"gen_mode"`
|
|
||||||
Seed int64 `json:"seed"`
|
|
||||||
UsePreLLM bool `json:"use_pre_llm"`
|
|
||||||
TemplateId string `json:"template_id"`
|
|
||||||
AspectRatio string `json:"aspect_ratio"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateTask 统一任务创建接口
|
// CreateTask 统一任务创建接口
|
||||||
func (h *JimengHandler) CreateTask(c *gin.Context) {
|
func (h *JimengHandler) CreateTask(c *gin.Context) {
|
||||||
var req JimengTaskRequest
|
var req types.JimengTaskRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
resp.ERROR(c, types.InvalidArgs)
|
resp.ERROR(c, types.InvalidArgs)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 文本审核
|
// 文本审核
|
||||||
if h.App.SysConfig.Moderation.Enable {
|
if h.App.SysConfig.Moderation.Enable && req.Prompt != "" {
|
||||||
moderationResult, err := h.moderationManager.GetService().Moderate(req.Prompt)
|
moderationResult, err := h.moderationManager.GetService().Moderate(req.Prompt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("failed to moderate content: ", err)
|
logger.Error("failed to moderate content: ", err)
|
||||||
@@ -103,166 +82,46 @@ func (h *JimengHandler) CreateTask(c *gin.Context) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新增:除图像特效外,其他任务类型必须有提示词
|
if req.Prompt == "" && len(req.ImageUrls) == 0 {
|
||||||
if req.TaskType != "image_effects" && req.Prompt == "" {
|
resp.ERROR(c, "提示词和图片不能同时为空")
|
||||||
resp.ERROR(c, "提示词不能为空")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := h.GetLoginUser(c)
|
user, err := h.GetLoginUser(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resp.NotAuth(c)
|
resp.NotAuth(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.Width == 0 {
|
// 获取算力消耗
|
||||||
req.Width = 1328
|
|
||||||
}
|
|
||||||
if req.Height == 0 {
|
|
||||||
req.Height = 1328
|
|
||||||
}
|
|
||||||
if req.Seed == 0 {
|
|
||||||
req.Seed = -1
|
|
||||||
}
|
|
||||||
|
|
||||||
var powerCost int
|
// if user.Power < powerCost {
|
||||||
var taskType model.JMTaskType
|
// resp.ERROR(c, fmt.Sprintf("算力不足,需要%d算力", powerCost))
|
||||||
var params map[string]any
|
// return
|
||||||
var reqKey string
|
// }
|
||||||
var modelName string
|
|
||||||
|
|
||||||
switch req.TaskType {
|
// taskReq := &jimeng.CreateTaskRequest{
|
||||||
case "text_to_image":
|
// Type: taskType,
|
||||||
powerCost = h.getPowerFromConfig(model.JMTaskTypeImage)
|
// Prompt: req.Prompt,
|
||||||
taskType = model.JMTaskTypeImage
|
// Params: params,
|
||||||
reqKey = jimeng.ReqKeyTextToImage
|
// ReqKey: reqKey,
|
||||||
modelName = "即梦文生图"
|
// Power: powerCost,
|
||||||
if req.Scale == 0 {
|
// }
|
||||||
req.Scale = 2.5
|
|
||||||
}
|
|
||||||
params = map[string]any{
|
|
||||||
"seed": req.Seed,
|
|
||||||
"scale": req.Scale,
|
|
||||||
"width": req.Width,
|
|
||||||
"height": req.Height,
|
|
||||||
"use_pre_llm": req.UsePreLLM,
|
|
||||||
}
|
|
||||||
case "image_to_image":
|
|
||||||
powerCost = h.getPowerFromConfig(model.JMTaskTypeVideo)
|
|
||||||
taskType = model.JMTaskTypeVideo
|
|
||||||
reqKey = jimeng.ReqKeyImageToImagePortrait
|
|
||||||
modelName = "即梦图生图"
|
|
||||||
if req.Gpen == 0 {
|
|
||||||
req.Gpen = 0.4
|
|
||||||
}
|
|
||||||
if req.Skin == 0 {
|
|
||||||
req.Skin = 0.3
|
|
||||||
}
|
|
||||||
if req.GenMode == "" {
|
|
||||||
if req.Prompt != "" {
|
|
||||||
req.GenMode = jimeng.GenModeCreative
|
|
||||||
} else {
|
|
||||||
req.GenMode = jimeng.GenModeReference
|
|
||||||
}
|
|
||||||
}
|
|
||||||
params = map[string]any{
|
|
||||||
"image_input": req.ImageInput,
|
|
||||||
"width": req.Width,
|
|
||||||
"height": req.Height,
|
|
||||||
"gpen": req.Gpen,
|
|
||||||
"skin": req.Skin,
|
|
||||||
"skin_unifi": req.SkinUnifi,
|
|
||||||
"gen_mode": req.GenMode,
|
|
||||||
"seed": req.Seed,
|
|
||||||
}
|
|
||||||
case "image_edit":
|
|
||||||
powerCost = h.getPowerFromConfig(model.JMTaskTypeVirtualHuman)
|
|
||||||
taskType = model.JMTaskTypeVirtualHuman
|
|
||||||
reqKey = jimeng.ReqKeyImageEdit
|
|
||||||
modelName = "即梦图像编辑"
|
|
||||||
if req.Scale == 0 {
|
|
||||||
req.Scale = 0.5
|
|
||||||
}
|
|
||||||
params = map[string]any{
|
|
||||||
"seed": req.Seed,
|
|
||||||
"scale": req.Scale,
|
|
||||||
}
|
|
||||||
params["image_urls"] = []string{req.ImageInput}
|
|
||||||
case "image_effects":
|
|
||||||
powerCost = h.getPowerFromConfig(model.JMTaskTypeActionTransfer)
|
|
||||||
taskType = model.JMTaskTypeActionTransfer
|
|
||||||
reqKey = jimeng.ReqKeyImageEffects
|
|
||||||
modelName = "即梦图像特效"
|
|
||||||
if req.Width == 0 {
|
|
||||||
req.Width = 1328
|
|
||||||
}
|
|
||||||
if req.Height == 0 {
|
|
||||||
req.Height = 1328
|
|
||||||
}
|
|
||||||
params = map[string]any{
|
|
||||||
"image_input1": req.ImageInput,
|
|
||||||
"template_id": req.TemplateId,
|
|
||||||
"width": req.Width,
|
|
||||||
"height": req.Height,
|
|
||||||
}
|
|
||||||
case "text_to_video":
|
|
||||||
powerCost = h.getPowerFromConfig(model.JMTaskTypeVideo)
|
|
||||||
taskType = model.JMTaskTypeVideo
|
|
||||||
reqKey = jimeng.ReqKeyTextToVideo
|
|
||||||
modelName = "即梦文生视频"
|
|
||||||
if req.AspectRatio == "" {
|
|
||||||
req.AspectRatio = jimeng.AspectRatio16_9
|
|
||||||
}
|
|
||||||
params = map[string]any{
|
|
||||||
"seed": req.Seed,
|
|
||||||
"aspect_ratio": req.AspectRatio,
|
|
||||||
}
|
|
||||||
case "image_to_video":
|
|
||||||
powerCost = h.getPowerFromConfig(model.JMTaskTypeVideo)
|
|
||||||
taskType = model.JMTaskTypeVideo
|
|
||||||
reqKey = jimeng.ReqKeyImageToVideo
|
|
||||||
modelName = "即梦图生视频"
|
|
||||||
params = map[string]any{
|
|
||||||
"seed": req.Seed,
|
|
||||||
"aspect_ratio": req.AspectRatio,
|
|
||||||
}
|
|
||||||
if len(req.ImageUrls) > 0 {
|
|
||||||
params["image_urls"] = req.ImageUrls
|
|
||||||
}
|
|
||||||
if len(req.BinaryDataBase64) > 0 {
|
|
||||||
params["binary_data_base64"] = req.BinaryDataBase64
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
resp.ERROR(c, "不支持的任务类型")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if user.Power < powerCost {
|
// job, err := h.jimengService.CreateTask(user.Id, taskReq)
|
||||||
resp.ERROR(c, fmt.Sprintf("算力不足,需要%d算力", powerCost))
|
// if err != nil {
|
||||||
return
|
// logger.Errorf("create jimeng task failed: %v", err)
|
||||||
}
|
// resp.ERROR(c, "创建任务失败")
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
taskReq := &jimeng.CreateTaskRequest{
|
// h.userService.DecreasePower(user.Id, powerCost, model.PowerLog{
|
||||||
Type: taskType,
|
// Type: types.PowerConsume,
|
||||||
Prompt: req.Prompt,
|
// Model: "jimeng",
|
||||||
Params: params,
|
// Remark: fmt.Sprintf("%s,任务ID:%d", modelName, job.Id),
|
||||||
ReqKey: reqKey,
|
// })
|
||||||
Power: powerCost,
|
|
||||||
}
|
|
||||||
|
|
||||||
job, err := h.jimengService.CreateTask(user.Id, taskReq)
|
resp.SUCCESS(c)
|
||||||
if err != nil {
|
|
||||||
logger.Errorf("create jimeng task failed: %v", err)
|
|
||||||
resp.ERROR(c, "创建任务失败")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h.userService.DecreasePower(user.Id, powerCost, model.PowerLog{
|
|
||||||
Type: types.PowerConsume,
|
|
||||||
Model: "jimeng",
|
|
||||||
Remark: fmt.Sprintf("%s,任务ID:%d", modelName, job.Id),
|
|
||||||
})
|
|
||||||
|
|
||||||
resp.SUCCESS(c, job)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Jobs 获取任务列表
|
// Jobs 获取任务列表
|
||||||
@@ -287,9 +146,9 @@ func (h *JimengHandler) Jobs(c *gin.Context) {
|
|||||||
|
|
||||||
switch req.Filter {
|
switch req.Filter {
|
||||||
case "image":
|
case "image":
|
||||||
query = query.Where("type = ?", model.JMTaskTypeImage)
|
query = query.Where("type = ?", types.JMTaskTypeImage)
|
||||||
case "video":
|
case "video":
|
||||||
query = query.Where("type = ?", model.JMTaskTypeVideo)
|
query = query.Where("type = ?", types.JMTaskTypeVideo)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(req.Ids) > 0 {
|
if len(req.Ids) > 0 {
|
||||||
@@ -349,7 +208,7 @@ func (h *JimengHandler) Remove(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 正在运行中的任务不能删除
|
// 正在运行中的任务不能删除
|
||||||
if job.Status == model.JMTaskStatusGenerating || job.Status == model.JMTaskStatusInQueue {
|
if job.Status == types.JMTaskStatusGenerating || job.Status == types.JMTaskStatusInQueue {
|
||||||
resp.ERROR(c, "正在运行中的任务不能删除,否则无法退回算力")
|
resp.ERROR(c, "正在运行中的任务不能删除,否则无法退回算力")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -362,7 +221,7 @@ func (h *JimengHandler) Remove(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 失败任务删除后退回算力
|
// 失败任务删除后退回算力
|
||||||
if job.Status != model.JMTaskStatusFailed {
|
if job.Status != types.JMTaskStatusFailed {
|
||||||
err = h.userService.IncreasePower(user.Id, job.Power, model.PowerLog{
|
err = h.userService.IncreasePower(user.Id, job.Power, model.PowerLog{
|
||||||
Type: types.PowerRefund,
|
Type: types.PowerRefund,
|
||||||
Model: "jimeng",
|
Model: "jimeng",
|
||||||
@@ -403,13 +262,13 @@ func (h *JimengHandler) Retry(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 只有失败的任务才能重试
|
// 只有失败的任务才能重试
|
||||||
if job.Status != model.JMTaskStatusFailed {
|
if job.Status != types.JMTaskStatusFailed {
|
||||||
resp.ERROR(c, "只有失败的任务才能重试")
|
resp.ERROR(c, "只有失败的任务才能重试")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重置任务状态
|
// 重置任务状态
|
||||||
if err := h.jimengService.UpdateJobStatus(uint(jobId), model.JMTaskStatusInQueue, ""); err != nil {
|
if err := h.jimengService.UpdateJobStatus(uint(jobId), types.JMTaskStatusInQueue, ""); err != nil {
|
||||||
logger.Errorf("reset job status failed: %v", err)
|
logger.Errorf("reset job status failed: %v", err)
|
||||||
resp.ERROR(c, "重置任务状态失败")
|
resp.ERROR(c, "重置任务状态失败")
|
||||||
return
|
return
|
||||||
@@ -426,17 +285,17 @@ func (h *JimengHandler) Retry(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// getPowerFromConfig 从配置中获取指定类型的算力消耗
|
// getPowerFromConfig 从配置中获取指定类型的算力消耗
|
||||||
func (h *JimengHandler) getPowerFromConfig(taskType model.JMTaskType) int {
|
func (h *JimengHandler) getPowerFromConfig(taskType types.JMTaskType) int {
|
||||||
config := h.App.SysConfig.Jimeng
|
config := h.App.SysConfig.Jimeng
|
||||||
|
|
||||||
switch taskType {
|
switch taskType {
|
||||||
case model.JMTaskTypeImage:
|
case types.JMTaskTypeImage:
|
||||||
return config.Power.Image
|
return config.Power.Image
|
||||||
case model.JMTaskTypeVideo:
|
case types.JMTaskTypeVideo:
|
||||||
return config.Power.Video
|
return config.Power.Video
|
||||||
case model.JMTaskTypeVirtualHuman:
|
case types.JMTaskTypeVirtualHuman:
|
||||||
return config.Power.VirtualHuman
|
return config.Power.VirtualHuman
|
||||||
case model.JMTaskTypeActionTransfer:
|
case types.JMTaskTypeActionTransfer:
|
||||||
return config.Power.ActionTransfer
|
return config.Power.ActionTransfer
|
||||||
default:
|
default:
|
||||||
return 10
|
return 10
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"geekai/core/types"
|
||||||
logger2 "geekai/logger"
|
logger2 "geekai/logger"
|
||||||
"geekai/service/oss"
|
"geekai/service/oss"
|
||||||
"geekai/store"
|
"geekai/store"
|
||||||
@@ -95,7 +96,7 @@ func (s *Service) processNextTask() {
|
|||||||
|
|
||||||
if err := s.ProcessTask(jobId); err != nil {
|
if err := s.ProcessTask(jobId); err != nil {
|
||||||
logger.Errorf("process jimeng task failed: job_id=%d, error=%v", jobId, err)
|
logger.Errorf("process jimeng task failed: job_id=%d, error=%v", jobId, err)
|
||||||
s.UpdateJobStatus(jobId, model.JMTaskStatusFailed, err.Error())
|
s.UpdateJobStatus(jobId, types.JMTaskStatusFailed, err.Error())
|
||||||
} else {
|
} else {
|
||||||
logger.Infof("Jimeng task processed successfully: job_id=%d", jobId)
|
logger.Infof("Jimeng task processed successfully: job_id=%d", jobId)
|
||||||
}
|
}
|
||||||
@@ -120,7 +121,7 @@ func (s *Service) CreateTask(userId uint, req *CreateTaskRequest) (*model.Jimeng
|
|||||||
ReqKey: req.ReqKey,
|
ReqKey: req.ReqKey,
|
||||||
Prompt: req.Prompt,
|
Prompt: req.Prompt,
|
||||||
Params: string(paramsJson),
|
Params: string(paramsJson),
|
||||||
Status: model.JMTaskStatusInQueue,
|
Status: types.JMTaskStatusInQueue,
|
||||||
Power: req.Power,
|
Power: req.Power,
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
UpdatedAt: time.Now(),
|
UpdatedAt: time.Now(),
|
||||||
@@ -148,7 +149,7 @@ func (s *Service) ProcessTask(jobId uint) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 更新任务状态为处理中
|
// 更新任务状态为处理中
|
||||||
if err := s.UpdateJobStatus(job.Id, model.JMTaskStatusGenerating, ""); err != nil {
|
if err := s.UpdateJobStatus(job.Id, types.JMTaskStatusGenerating, ""); err != nil {
|
||||||
return fmt.Errorf("update job status failed: %w", err)
|
return fmt.Errorf("update job status failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,13 +200,13 @@ func (s *Service) buildTaskRequest(job *model.JimengJob) (*SubmitTaskRequest, er
|
|||||||
|
|
||||||
// 根据任务类型设置特定参数
|
// 根据任务类型设置特定参数
|
||||||
switch job.Type {
|
switch job.Type {
|
||||||
case model.JMTaskTypeImage:
|
case types.JMTaskTypeImage:
|
||||||
s.setTextToImageParams(req, params)
|
s.setTextToImageParams(req, params)
|
||||||
case model.JMTaskTypeVideo:
|
case types.JMTaskTypeVideo:
|
||||||
s.setImageToImageParams(req, params)
|
s.setImageToImageParams(req, params)
|
||||||
case model.JMTaskTypeVirtualHuman:
|
case types.JMTaskTypeVirtualHuman:
|
||||||
s.setImageEditParams(req, params)
|
s.setImageEditParams(req, params)
|
||||||
case model.JMTaskTypeActionTransfer:
|
case types.JMTaskTypeActionTransfer:
|
||||||
s.setImageEffectsParams(req, params)
|
s.setImageEffectsParams(req, params)
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported task type: %s", job.Type)
|
return nil, fmt.Errorf("unsupported task type: %s", job.Type)
|
||||||
@@ -353,7 +354,7 @@ func (s *Service) pollTaskStatus() {
|
|||||||
|
|
||||||
for {
|
for {
|
||||||
var jobs []model.JimengJob
|
var jobs []model.JimengJob
|
||||||
s.db.Where("status IN (?)", []model.JMTaskStatus{model.JMTaskStatusGenerating, model.JMTaskStatusInQueue}).Find(&jobs)
|
s.db.Where("status IN (?)", []types.JMTaskStatus{types.JMTaskStatusGenerating, types.JMTaskStatusInQueue}).Find(&jobs)
|
||||||
if len(jobs) == 0 {
|
if len(jobs) == 0 {
|
||||||
logger.Debugf("no jimeng task to poll, sleep 10s")
|
logger.Debugf("no jimeng task to poll, sleep 10s")
|
||||||
time.Sleep(10 * time.Second)
|
time.Sleep(10 * time.Second)
|
||||||
@@ -389,7 +390,7 @@ func (s *Service) pollTaskStatus() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch resp.Data.Status {
|
switch resp.Data.Status {
|
||||||
case model.JMTaskStatusDone:
|
case types.JMTaskStatusDone:
|
||||||
// 判断任务是否成功
|
// 判断任务是否成功
|
||||||
if resp.Message != "Success" {
|
if resp.Message != "Success" {
|
||||||
s.handleTaskError(job.Id, fmt.Sprintf("task failed: %s", resp.Data.AlgorithmBaseResp.StatusMessage))
|
s.handleTaskError(job.Id, fmt.Sprintf("task failed: %s", resp.Data.AlgorithmBaseResp.StatusMessage))
|
||||||
@@ -398,7 +399,7 @@ func (s *Service) pollTaskStatus() {
|
|||||||
|
|
||||||
// 任务完成,更新结果
|
// 任务完成,更新结果
|
||||||
updates := map[string]any{
|
updates := map[string]any{
|
||||||
"status": model.JMTaskStatusSuccess,
|
"status": types.JMTaskStatusSuccess,
|
||||||
"updated_at": time.Now(),
|
"updated_at": time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -421,15 +422,15 @@ func (s *Service) pollTaskStatus() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
s.db.Model(&model.JimengJob{}).Where("id = ?", job.Id).Updates(updates)
|
s.db.Model(&model.JimengJob{}).Where("id = ?", job.Id).Updates(updates)
|
||||||
case model.JMTaskStatusInQueue, model.JMTaskStatusGenerating:
|
case types.JMTaskStatusInQueue, types.JMTaskStatusGenerating:
|
||||||
// 任务处理中
|
// 任务处理中
|
||||||
s.UpdateJobStatus(job.Id, model.JMTaskStatusGenerating, "")
|
s.UpdateJobStatus(job.Id, types.JMTaskStatusGenerating, "")
|
||||||
|
|
||||||
case model.JMTaskStatusNotFound:
|
case types.JMTaskStatusNotFound:
|
||||||
// 任务未找到
|
// 任务未找到
|
||||||
s.handleTaskError(job.Id, "task not found")
|
s.handleTaskError(job.Id, "task not found")
|
||||||
|
|
||||||
case model.JMTaskStatusExpired:
|
case types.JMTaskStatusExpired:
|
||||||
continue
|
continue
|
||||||
default:
|
default:
|
||||||
logger.Warnf("unknown task status: %s", resp.Data.Status)
|
logger.Warnf("unknown task status: %s", resp.Data.Status)
|
||||||
@@ -444,7 +445,7 @@ func (s *Service) pollTaskStatus() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UpdateJobStatus 更新任务状态
|
// UpdateJobStatus 更新任务状态
|
||||||
func (s *Service) UpdateJobStatus(jobId uint, status model.JMTaskStatus, errMsg string) error {
|
func (s *Service) UpdateJobStatus(jobId uint, status types.JMTaskStatus, errMsg string) error {
|
||||||
updates := map[string]any{
|
updates := map[string]any{
|
||||||
"status": status,
|
"status": status,
|
||||||
"updated_at": time.Now(),
|
"updated_at": time.Now(),
|
||||||
@@ -458,7 +459,7 @@ func (s *Service) UpdateJobStatus(jobId uint, status model.JMTaskStatus, errMsg
|
|||||||
// handleTaskError 处理任务错误
|
// handleTaskError 处理任务错误
|
||||||
func (s *Service) handleTaskError(jobId uint, errMsg string) error {
|
func (s *Service) handleTaskError(jobId uint, errMsg string) error {
|
||||||
logger.Errorf("Jimeng task error (job_id: %d): %s", jobId, errMsg)
|
logger.Errorf("Jimeng task error (job_id: %d): %s", jobId, errMsg)
|
||||||
return s.UpdateJobStatus(jobId, model.JMTaskStatusFailed, errMsg)
|
return s.UpdateJobStatus(jobId, types.JMTaskStatusFailed, errMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PushTaskToQueue 推送任务到队列(用于手动重试)
|
// PushTaskToQueue 推送任务到队列(用于手动重试)
|
||||||
@@ -469,7 +470,7 @@ func (s *Service) PushTaskToQueue(jobId uint) error {
|
|||||||
// GetTaskStats 获取任务统计信息
|
// GetTaskStats 获取任务统计信息
|
||||||
func (s *Service) GetTaskStats() (map[string]any, error) {
|
func (s *Service) GetTaskStats() (map[string]any, error) {
|
||||||
type StatResult struct {
|
type StatResult struct {
|
||||||
Status string `json:"status"`
|
Status types.JMTaskStatus `json:"status"`
|
||||||
Count int64 `json:"count"`
|
Count int64 `json:"count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,7 +493,7 @@ func (s *Service) GetTaskStats() (map[string]any, error) {
|
|||||||
|
|
||||||
for _, stat := range stats {
|
for _, stat := range stats {
|
||||||
result["total"] = result["total"].(int64) + stat.Count
|
result["total"] = result["total"].(int64) + stat.Count
|
||||||
result[stat.Status] = stat.Count
|
result[string(stat.Status)] = stat.Count
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
|
|||||||
@@ -1,15 +1,7 @@
|
|||||||
package jimeng
|
package jimeng
|
||||||
|
|
||||||
import "geekai/store/model"
|
import (
|
||||||
|
"geekai/core/types"
|
||||||
// ReqKey 常量定义
|
|
||||||
const (
|
|
||||||
ReqKeyTextToImage = "high_aes_general_v30l_zt2i" // 文生图
|
|
||||||
ReqKeyImageToImagePortrait = "i2i_portrait_photo" // 图生图人像写真
|
|
||||||
ReqKeyImageEdit = "seededit_v3.0" // 图像编辑
|
|
||||||
ReqKeyImageEffects = "i2i_multi_style_zx2x" // 图像特效
|
|
||||||
ReqKeyTextToVideo = "jimeng_vgfm_t2v_l20" // 文生视频
|
|
||||||
ReqKeyImageToVideo = "jimeng_vgfm_i2v_l20" // 图生视频
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// SubmitTaskRequest 提交任务请求
|
// SubmitTaskRequest 提交任务请求
|
||||||
@@ -73,7 +65,7 @@ type QueryTaskResponse struct {
|
|||||||
ImageUrls []string `json:"image_urls"`
|
ImageUrls []string `json:"image_urls"`
|
||||||
VideoUrl string `json:"video_url"`
|
VideoUrl string `json:"video_url"`
|
||||||
RespData string `json:"resp_data"`
|
RespData string `json:"resp_data"`
|
||||||
Status model.JMTaskStatus `json:"status"`
|
Status types.JMTaskStatus `json:"status"`
|
||||||
LlmResult string `json:"llm_result"`
|
LlmResult string `json:"llm_result"`
|
||||||
PeResult string `json:"pe_result"`
|
PeResult string `json:"pe_result"`
|
||||||
PredictTagsResult string `json:"predict_tags_result"`
|
PredictTagsResult string `json:"predict_tags_result"`
|
||||||
@@ -85,61 +77,10 @@ type QueryTaskResponse struct {
|
|||||||
|
|
||||||
// CreateTaskRequest 创建任务请求
|
// CreateTaskRequest 创建任务请求
|
||||||
type CreateTaskRequest struct {
|
type CreateTaskRequest struct {
|
||||||
Type model.JMTaskType `json:"type"`
|
Type types.JMTaskType `json:"type"`
|
||||||
Prompt string `json:"prompt"`
|
Prompt string `json:"prompt"`
|
||||||
Params map[string]any `json:"params"`
|
Params map[string]any `json:"params"`
|
||||||
ReqKey string `json:"req_key"`
|
ReqKey string `json:"req_key"`
|
||||||
ImageUrls []string `json:"image_urls,omitempty"`
|
ImageUrls []string `json:"image_urls,omitempty"`
|
||||||
Power int `json:"power,omitempty"`
|
Power int `json:"power,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// LogoInfo 水印信息
|
|
||||||
type LogoInfo struct {
|
|
||||||
AddLogo bool `json:"add_logo"`
|
|
||||||
Position int `json:"position"`
|
|
||||||
Language int `json:"language"`
|
|
||||||
Opacity float64 `json:"opacity"`
|
|
||||||
LogoTextContent string `json:"logo_text_content"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReqJsonConfig 查询配置
|
|
||||||
type ReqJsonConfig struct {
|
|
||||||
ReturnUrl bool `json:"return_url"`
|
|
||||||
LogoInfo *LogoInfo `json:"logo_info,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ImageEffectTemplate 图像特效模板
|
|
||||||
const (
|
|
||||||
TemplateIdFelt3DPolaroid = "felt_3d_polaroid" // 毛毡3d拍立得风格
|
|
||||||
TemplateIdMyWorld = "my_world" // 像素世界风
|
|
||||||
TemplateIdMyWorldUniversal = "my_world_universal" // 像素世界-万物通用版
|
|
||||||
TemplateIdPlasticBubbleFigure = "plastic_bubble_figure" // 盲盒玩偶风
|
|
||||||
TemplateIdPlasticBubbleFigureCartoon = "plastic_bubble_figure_cartoon_text" // 塑料泡罩人偶-文字卡头版
|
|
||||||
TemplateIdFurryDreamDoll = "furry_dream_doll" // 毛绒玩偶风
|
|
||||||
TemplateIdMicroLandscapeMiniWorld = "micro_landscape_mini_world" // 迷你世界玩偶风
|
|
||||||
TemplateIdMicroLandscapeProfessional = "micro_landscape_mini_world_professional" // 微型景观小世界-职业版
|
|
||||||
TemplateIdAcrylicOrnaments = "acrylic_ornaments" // 亚克力挂饰
|
|
||||||
TemplateIdFeltKeychain = "felt_keychain" // 毛毡钥匙扣
|
|
||||||
TemplateIdLofiPixelCharacter = "lofi_pixel_character_mini_card" // Lofi像素人物小卡
|
|
||||||
TemplateIdAngelFigurine = "angel_figurine" // 天使形象手办
|
|
||||||
TemplateIdLyingInFluffyBelly = "lying_in_fluffy_belly" // 躺在毛茸茸肚皮里
|
|
||||||
TemplateIdGlassBall = "glass_ball" // 玻璃球
|
|
||||||
)
|
|
||||||
|
|
||||||
// AspectRatio 视频宽高比
|
|
||||||
const (
|
|
||||||
AspectRatio16_9 = "16:9" // 1280×720
|
|
||||||
AspectRatio9_16 = "9:16" // 720×1280
|
|
||||||
AspectRatio1_1 = "1:1" // 960×960
|
|
||||||
AspectRatio4_3 = "4:3" // 960×720
|
|
||||||
AspectRatio3_4 = "3:4" // 720×960
|
|
||||||
AspectRatio21_9 = "21:9" // 1680×720
|
|
||||||
AspectRatio9_21 = "9:21" // 720×1680
|
|
||||||
)
|
|
||||||
|
|
||||||
// GenMode 生成模式
|
|
||||||
const (
|
|
||||||
GenModeCreative = "creative" // 提示词模式
|
|
||||||
GenModeReference = "reference" // 全参考模式
|
|
||||||
GenModeReferenceChar = "reference_char" // 人物参考模式
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"geekai/core/types"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -9,7 +10,7 @@ type JimengJob struct {
|
|||||||
Id uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
Id uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||||
UserId uint `gorm:"column:user_id;type:int(11);not null;index;comment:用户ID" json:"user_id"`
|
UserId uint `gorm:"column:user_id;type:int(11);not null;index;comment:用户ID" json:"user_id"`
|
||||||
TaskId string `gorm:"column:task_id;type:varchar(100);not null;index;comment:任务ID" json:"task_id"`
|
TaskId string `gorm:"column:task_id;type:varchar(100);not null;index;comment:任务ID" json:"task_id"`
|
||||||
Type JMTaskType `gorm:"column:type;type:varchar(50);not null;comment:任务类型" json:"type"`
|
Type types.JMTaskType `gorm:"column:type;type:varchar(50);not null;comment:任务类型" json:"type"`
|
||||||
ReqKey string `gorm:"column:req_key;type:varchar(100);comment:请求Key" json:"req_key"`
|
ReqKey string `gorm:"column:req_key;type:varchar(100);comment:请求Key" json:"req_key"`
|
||||||
Prompt string `gorm:"column:prompt;type:text;comment:提示词" json:"prompt"`
|
Prompt string `gorm:"column:prompt;type:text;comment:提示词" json:"prompt"`
|
||||||
Params string `gorm:"column:params;type:text;comment:任务参数JSON" json:"params"`
|
Params string `gorm:"column:params;type:text;comment:任务参数JSON" json:"params"`
|
||||||
@@ -17,36 +18,13 @@ type JimengJob struct {
|
|||||||
VideoURL string `gorm:"column:video_url;type:varchar(1024);comment:视频URL" json:"video_url"`
|
VideoURL string `gorm:"column:video_url;type:varchar(1024);comment:视频URL" json:"video_url"`
|
||||||
RawData string `gorm:"column:raw_data;type:text;comment:原始API响应" json:"raw_data"`
|
RawData string `gorm:"column:raw_data;type:text;comment:原始API响应" json:"raw_data"`
|
||||||
Progress int `gorm:"column:progress;type:int;default:0;comment:进度百分比" json:"progress"`
|
Progress int `gorm:"column:progress;type:int;default:0;comment:进度百分比" json:"progress"`
|
||||||
Status JMTaskStatus `gorm:"column:status;type:varchar(20);default:'pending';comment:任务状态" json:"status"`
|
Status types.JMTaskStatus `gorm:"column:status;type:varchar(20);default:'pending';comment:任务状态" json:"status"`
|
||||||
ErrMsg string `gorm:"column:err_msg;type:varchar(1024);comment:错误信息" json:"err_msg"`
|
ErrMsg string `gorm:"column:err_msg;type:varchar(1024);comment:错误信息" json:"err_msg"`
|
||||||
Power int `gorm:"column:power;type:int(11);default:0;comment:消耗算力" json:"power"`
|
Power int `gorm:"column:power;type:int(11);default:0;comment:消耗算力" json:"power"`
|
||||||
CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null;comment:创建时间" json:"created_at"`
|
CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null;comment:创建时间" json:"created_at"`
|
||||||
UpdatedAt time.Time `gorm:"column:updated_at;type:datetime;not null;comment:更新时间" json:"updated_at"`
|
UpdatedAt time.Time `gorm:"column:updated_at;type:datetime;not null;comment:更新时间" json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// JMTaskStatus 任务状态
|
|
||||||
type JMTaskStatus string
|
|
||||||
|
|
||||||
const (
|
|
||||||
JMTaskStatusInQueue = JMTaskStatus("in_queue") // 任务已提交
|
|
||||||
JMTaskStatusGenerating = JMTaskStatus("generating") // 任务处理中
|
|
||||||
JMTaskStatusDone = JMTaskStatus("done") // 处理完成
|
|
||||||
JMTaskStatusNotFound = JMTaskStatus("not_found") // 任务未找到
|
|
||||||
JMTaskStatusSuccess = JMTaskStatus("success") // 任务成功
|
|
||||||
JMTaskStatusFailed = JMTaskStatus("failed") // 任务失败
|
|
||||||
JMTaskStatusExpired = JMTaskStatus("expired") // 任务过期
|
|
||||||
)
|
|
||||||
|
|
||||||
// JMTaskType 任务类型
|
|
||||||
type JMTaskType string
|
|
||||||
|
|
||||||
const (
|
|
||||||
JMTaskTypeImage = JMTaskType("image") // 文生图
|
|
||||||
JMTaskTypeVideo = JMTaskType("video") // 图生图
|
|
||||||
JMTaskTypeVirtualHuman = JMTaskType("virtual_human") // 图像编辑
|
|
||||||
JMTaskTypeActionTransfer = JMTaskType("action_transfer") // 图像特效
|
|
||||||
)
|
|
||||||
|
|
||||||
// TableName 返回数据表名称
|
// TableName 返回数据表名称
|
||||||
func (JimengJob) TableName() string {
|
func (JimengJob) TableName() string {
|
||||||
return "geekai_jimeng_jobs"
|
return "geekai_jimeng_jobs"
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package vo
|
package vo
|
||||||
|
|
||||||
import "geekai/store/model"
|
import "geekai/core/types"
|
||||||
|
|
||||||
// JimengJob 即梦AI任务VO
|
// JimengJob 即梦AI任务VO
|
||||||
type JimengJob struct {
|
type JimengJob struct {
|
||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
UserId uint `json:"user_id"`
|
UserId uint `json:"user_id"`
|
||||||
TaskId string `json:"task_id"`
|
TaskId string `json:"task_id"`
|
||||||
Type model.JMTaskType `json:"type"`
|
Type types.JMTaskType `json:"type"`
|
||||||
ReqKey string `json:"req_key"`
|
ReqKey string `json:"req_key"`
|
||||||
Prompt string `json:"prompt"`
|
Prompt string `json:"prompt"`
|
||||||
Params map[string]any `json:"params"`
|
Params map[string]any `json:"params"`
|
||||||
@@ -15,7 +15,7 @@ type JimengJob struct {
|
|||||||
VideoURL string `json:"video_url"`
|
VideoURL string `json:"video_url"`
|
||||||
RawData string `json:"raw_data"`
|
RawData string `json:"raw_data"`
|
||||||
Progress int `json:"progress"`
|
Progress int `json:"progress"`
|
||||||
Status model.JMTaskStatus `json:"status"`
|
Status types.JMTaskStatus `json:"status"`
|
||||||
ErrMsg string `json:"err_msg"`
|
ErrMsg string `json:"err_msg"`
|
||||||
Power int `json:"power"`
|
Power int `json:"power"`
|
||||||
CreatedAt int64 `json:"created_at"` // 时间戳
|
CreatedAt int64 `json:"created_at"` // 时间戳
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 143 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 933 KiB After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 982 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 7.4 KiB |
@@ -178,10 +178,13 @@ const props = defineProps({
|
|||||||
const selectedModel = ref(props.items[0])
|
const selectedModel = ref(props.items[0])
|
||||||
const requiredKeys = ref(props.requiredKeys)
|
const requiredKeys = ref(props.requiredKeys)
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue'])
|
const emit = defineEmits(['update:modelValue', 'update:requiredKeys'])
|
||||||
|
|
||||||
// 初始化 modelValue 默认值
|
// 初始化 modelValue 默认值
|
||||||
const initModelValue = (model) => {
|
const initModelValue = (model) => {
|
||||||
|
if (props.items.length === 0) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
const defaultValues = {}
|
const defaultValues = {}
|
||||||
requiredKeys.value = {}
|
requiredKeys.value = {}
|
||||||
if (model && model.params) {
|
if (model && model.params) {
|
||||||
@@ -250,6 +253,7 @@ watch(
|
|||||||
() => props.items,
|
() => props.items,
|
||||||
(newValue) => {
|
(newValue) => {
|
||||||
selectedModel.value = newValue[0]
|
selectedModel.value = newValue[0]
|
||||||
|
modelValue.value = initModelValue(selectedModel.value)
|
||||||
},
|
},
|
||||||
{ deep: true }
|
{ deep: true }
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -102,76 +102,7 @@ export const JimengParams = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: '图片 2.1 文生图',
|
|
||||||
version: '2.1',
|
|
||||||
label: '平面绘感强,可生成文字海报',
|
|
||||||
key: 'jimeng_high_aes_general_v21_L',
|
|
||||||
params: [
|
|
||||||
{
|
|
||||||
name: 'prompt',
|
|
||||||
label: '提示词',
|
|
||||||
type: 'textarea',
|
|
||||||
showWordLimit: true,
|
|
||||||
maxlength: 800,
|
|
||||||
autosize: { minRows: 3, maxRows: 5 },
|
|
||||||
required: true,
|
|
||||||
placeholder: '请输入提示词',
|
|
||||||
info: '用于生成图像的提示词 ,中英文均可输入',
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: 'size',
|
|
||||||
type: 'select',
|
|
||||||
required: true,
|
|
||||||
placeholder: '请选择尺寸',
|
|
||||||
label: '图片尺寸',
|
|
||||||
prefix: 'icon-resize',
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
label: '21:9 (1195 * 512)',
|
|
||||||
value: '1195x512',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '16:9 (1024 * 576)',
|
|
||||||
value: '1024x576',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '3:2 (1024 * 682)',
|
|
||||||
value: '1024x682',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '4:3 (1024 * 768)',
|
|
||||||
value: '1024x768',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '1:1 (1024 * 1024)',
|
|
||||||
value: '1024x1024',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '3:4 (768 * 1024)',
|
|
||||||
value: '768x1024',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '2:3 (682 * 1024)',
|
|
||||||
value: '682x1024',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '9:16 (576 * 1024)',
|
|
||||||
value: '576x1024',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'use_pre_llm',
|
|
||||||
type: 'switch',
|
|
||||||
required: false,
|
|
||||||
label: '开启文本扩写',
|
|
||||||
info: '开启后,系统会自动扩写提示词,提高生成质量',
|
|
||||||
value: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: '图片 3.0 文生图',
|
name: '图片 3.0 文生图',
|
||||||
version: '3.0',
|
version: '3.0',
|
||||||
@@ -354,6 +285,17 @@ export const JimengParams = {
|
|||||||
accept: '.png,.jpg,.jpeg',
|
accept: '.png,.jpg,.jpeg',
|
||||||
info: '长边与短边比例在3以内,超出此比例或比例相对极端,会导致报错。',
|
info: '长边与短边比例在3以内,超出此比例或比例相对极端,会导致报错。',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'scale',
|
||||||
|
label: '文本描述影响的程度',
|
||||||
|
type: 'slider',
|
||||||
|
required: true,
|
||||||
|
info: '该值越大代表文本描述影响程度越大,且输入图片影响程度越小',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
step: 0.1,
|
||||||
|
value: 0.5,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'size',
|
name: 'size',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
@@ -405,17 +347,6 @@ export const JimengParams = {
|
|||||||
label: '将输入的单人写真图片,进行有创意的特效化处理。',
|
label: '将输入的单人写真图片,进行有创意的特效化处理。',
|
||||||
key: 'i2i_multi_style_zx2x',
|
key: 'i2i_multi_style_zx2x',
|
||||||
params: [
|
params: [
|
||||||
{
|
|
||||||
name: 'prompt',
|
|
||||||
label: '提示词',
|
|
||||||
type: 'textarea',
|
|
||||||
required: true,
|
|
||||||
showWordLimit: true,
|
|
||||||
maxlength: 800,
|
|
||||||
autosize: { minRows: 3, maxRows: 5 },
|
|
||||||
placeholder: '请输入用于编辑图像的提示词,如:把xxx改成xxx,删除xxx,添加xxx等',
|
|
||||||
info: '建议长度<=120字符,最长不超过800字符',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'image_urls',
|
name: 'image_urls',
|
||||||
label: '参考图片',
|
label: '参考图片',
|
||||||
@@ -610,6 +541,77 @@ export const JimengParams = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: '图片 2.1 文生图',
|
||||||
|
version: '2.1',
|
||||||
|
label: '平面绘感强,可生成文字海报',
|
||||||
|
key: 'jimeng_high_aes_general_v21_L',
|
||||||
|
params: [
|
||||||
|
{
|
||||||
|
name: 'prompt',
|
||||||
|
label: '提示词',
|
||||||
|
type: 'textarea',
|
||||||
|
showWordLimit: true,
|
||||||
|
maxlength: 800,
|
||||||
|
autosize: { minRows: 3, maxRows: 5 },
|
||||||
|
required: true,
|
||||||
|
placeholder: '请输入提示词',
|
||||||
|
info: '用于生成图像的提示词 ,中英文均可输入',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'size',
|
||||||
|
type: 'select',
|
||||||
|
required: true,
|
||||||
|
placeholder: '请选择尺寸',
|
||||||
|
label: '图片尺寸',
|
||||||
|
prefix: 'icon-resize',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: '21:9 (1195 * 512)',
|
||||||
|
value: '1195x512',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '16:9 (1024 * 576)',
|
||||||
|
value: '1024x576',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '3:2 (1024 * 682)',
|
||||||
|
value: '1024x682',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '4:3 (1024 * 768)',
|
||||||
|
value: '1024x768',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '1:1 (1024 * 1024)',
|
||||||
|
value: '1024x1024',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '3:4 (768 * 1024)',
|
||||||
|
value: '768x1024',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '2:3 (682 * 1024)',
|
||||||
|
value: '682x1024',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '9:16 (576 * 1024)',
|
||||||
|
value: '576x1024',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'use_pre_llm',
|
||||||
|
type: 'switch',
|
||||||
|
required: false,
|
||||||
|
label: '开启文本扩写',
|
||||||
|
info: '开启后,系统会自动扩写提示词,提高生成质量',
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
video: [
|
video: [
|
||||||
// 视频 3.0 720P-文生视频
|
// 视频 3.0 720P-文生视频
|
||||||
|
|||||||
@@ -28,8 +28,6 @@ export const useJimengStore = defineStore('jimeng', () => {
|
|||||||
|
|
||||||
// 用户信息
|
// 用户信息
|
||||||
const isLogin = ref(false)
|
const isLogin = ref(false)
|
||||||
const userPower = ref(100)
|
|
||||||
|
|
||||||
// 视频预览
|
// 视频预览
|
||||||
const showDialog = ref(false)
|
const showDialog = ref(false)
|
||||||
const currentVideoUrl = ref('')
|
const currentVideoUrl = ref('')
|
||||||
@@ -37,16 +35,9 @@ export const useJimengStore = defineStore('jimeng', () => {
|
|||||||
// 登录弹窗
|
// 登录弹窗
|
||||||
const shareStore = useSharedStore()
|
const shareStore = useSharedStore()
|
||||||
|
|
||||||
// 新增:动态获取算力消耗配置
|
// 积分消耗配置
|
||||||
const powerConfig = reactive({})
|
const powerConfig = reactive({})
|
||||||
// 动态设置算力消耗
|
const currentPowerCost = ref('0积分')
|
||||||
const setFunctionPowers = (config) => {
|
|
||||||
functions.forEach((f) => {
|
|
||||||
if (config[f.key] !== undefined) {
|
|
||||||
f.power = config[f.key]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 功能配置
|
// 功能配置
|
||||||
const functions = JimengFunctions
|
const functions = JimengFunctions
|
||||||
@@ -69,11 +60,7 @@ export const useJimengStore = defineStore('jimeng', () => {
|
|||||||
const switchFunction = (f) => {
|
const switchFunction = (f) => {
|
||||||
activeFunction.value = f.key
|
activeFunction.value = f.key
|
||||||
formData.value = {}
|
formData.value = {}
|
||||||
}
|
setFunctionPowers()
|
||||||
|
|
||||||
// 获取当前算力消耗
|
|
||||||
const getCurrentPowerCost = () => {
|
|
||||||
return activeFunction.value.power
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取功能名称
|
// 获取功能名称
|
||||||
@@ -198,12 +185,9 @@ export const useJimengStore = defineStore('jimeng', () => {
|
|||||||
shareStore.setShowLoginDialog(true)
|
shareStore.setShowLoginDialog(true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// if (userPower.value < currentPowerCost.value) {
|
console.log(formData.value)
|
||||||
// showMessageError('算力不足')
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
for (const key in requiredKeys.value) {
|
for (const key in requiredKeys.value) {
|
||||||
if (!formData.value[key].required) {
|
if (!formData.value[key]) {
|
||||||
showMessageError('缺少参数:' + requiredKeys.value[key].label)
|
showMessageError('缺少参数:' + requiredKeys.value[key].label)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -296,18 +280,25 @@ export const useJimengStore = defineStore('jimeng', () => {
|
|||||||
showDialog.value = true
|
showDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const setFunctionPowers = () => {
|
||||||
|
if (activeFunction.value === 'image') {
|
||||||
|
currentPowerCost.value = `${powerConfig.image}积分/张`
|
||||||
|
} else {
|
||||||
|
currentPowerCost.value = `${powerConfig.video}积分/秒`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 初始化方法
|
// 初始化方法
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
try {
|
try {
|
||||||
// 获取算力消耗配置
|
// 获取积分消耗配置
|
||||||
const powerRes = await httpGet('/api/jimeng/power-config')
|
const powerRes = await httpGet('/api/jimeng/power-config')
|
||||||
if (powerRes.data) {
|
if (powerRes.data) {
|
||||||
Object.assign(powerConfig, powerRes.data)
|
Object.assign(powerConfig, powerRes.data)
|
||||||
setFunctionPowers(powerRes.data)
|
setFunctionPowers()
|
||||||
}
|
}
|
||||||
const user = await checkSession()
|
const user = await checkSession()
|
||||||
isLogin.value = true
|
isLogin.value = true
|
||||||
userPower.value = user.power
|
|
||||||
// 获取任务列表
|
// 获取任务列表
|
||||||
await fetchData(1)
|
await fetchData(1)
|
||||||
// 开始轮询
|
// 开始轮询
|
||||||
@@ -335,7 +326,6 @@ export const useJimengStore = defineStore('jimeng', () => {
|
|||||||
currentList,
|
currentList,
|
||||||
isOver,
|
isOver,
|
||||||
isLogin,
|
isLogin,
|
||||||
userPower,
|
|
||||||
showDialog,
|
showDialog,
|
||||||
currentVideoUrl,
|
currentVideoUrl,
|
||||||
|
|
||||||
@@ -346,11 +336,11 @@ export const useJimengStore = defineStore('jimeng', () => {
|
|||||||
formData,
|
formData,
|
||||||
requiredKeys,
|
requiredKeys,
|
||||||
progress,
|
progress,
|
||||||
|
currentPowerCost,
|
||||||
|
|
||||||
// 方法
|
// 方法
|
||||||
init,
|
init,
|
||||||
switchFunction,
|
switchFunction,
|
||||||
getCurrentPowerCost,
|
|
||||||
getFunctionName,
|
getFunctionName,
|
||||||
getTaskStatusText,
|
getTaskStatusText,
|
||||||
getTaskType,
|
getTaskType,
|
||||||
|
|||||||
@@ -140,7 +140,7 @@
|
|||||||
<!-- 功能开关 -->
|
<!-- 功能开关 -->
|
||||||
<div class="function-params">
|
<div class="function-params">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class="mb-2">
|
<div class="mb-2" v-if="store.functionParams[store.activeFunction].length > 0">
|
||||||
<label class="label text-left font-bold">模型选择</label>
|
<label class="label text-left font-bold">模型选择</label>
|
||||||
</div>
|
</div>
|
||||||
<param-builder
|
<param-builder
|
||||||
@@ -152,7 +152,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 提交按钮 -->
|
<!-- 提交按钮 -->
|
||||||
<div class="submit-btn flex justify-center pt-4">
|
<div
|
||||||
|
class="submit-btn flex justify-center pt-4"
|
||||||
|
v-if="store.functionParams[store.activeFunction].length > 0"
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
@click="store.submitTask"
|
@click="store.submitTask"
|
||||||
:disabled="store.submitting"
|
:disabled="store.submitting"
|
||||||
@@ -161,7 +164,7 @@
|
|||||||
>
|
>
|
||||||
<i v-if="store.submitting" class="iconfont icon-loading animate-spin"></i>
|
<i v-if="store.submitting" class="iconfont icon-loading animate-spin"></i>
|
||||||
<i v-else class="iconfont icon-chuangzuo"></i>
|
<i v-else class="iconfont icon-chuangzuo"></i>
|
||||||
<span>立即生成 ({{ store.currentPowerCost }}算力)</span>
|
<span>立即生成 ({{ store.currentPowerCost }})</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -342,7 +345,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="task-meta">
|
<div class="task-meta">
|
||||||
<span>{{ dateFormat(item.created_at) }}</span>
|
<span>{{ dateFormat(item.created_at) }}</span>
|
||||||
<span v-if="item.power">{{ item.power }}算力</span>
|
<span v-if="item.power">{{ item.power }}积分</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -57,9 +57,8 @@
|
|||||||
<el-form-item>
|
<el-form-item>
|
||||||
<template #label>
|
<template #label>
|
||||||
<div class="text-gray-500 text-sm">
|
<div class="text-gray-500 text-sm">
|
||||||
生成图片消耗的积分,包括:文生图、图生图、图片编辑、图片特效,<span
|
生成图片消耗的积分,包括:文生图、图生图、图片编辑、图片特效,<el-tag type="primary"
|
||||||
class="text-red-500"
|
>单位:积分/张</el-tag
|
||||||
>单位:积分/张</span
|
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -72,8 +71,8 @@
|
|||||||
<el-form-item>
|
<el-form-item>
|
||||||
<template #label>
|
<template #label>
|
||||||
<div class="text-gray-500 text-sm">
|
<div class="text-gray-500 text-sm">
|
||||||
生成视频消耗的积分,包括:文生视频、图生视频,<span class="text-red-500"
|
生成视频消耗的积分,包括:文生视频、图生视频,<el-tag type="primary"
|
||||||
>单位:积分/秒</span
|
>单位:积分/秒</el-tag
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -86,7 +85,7 @@
|
|||||||
<el-form-item>
|
<el-form-item>
|
||||||
<template #label>
|
<template #label>
|
||||||
<div class="text-gray-500 text-sm">
|
<div class="text-gray-500 text-sm">
|
||||||
生成数字人视频消耗的积分,<span class="text-red-500">单位:积分/秒</span>
|
生成数字人视频消耗的积分,<el-tag type="primary">单位:积分/秒</el-tag>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<el-input-number
|
<el-input-number
|
||||||
@@ -98,7 +97,7 @@
|
|||||||
<el-form-item>
|
<el-form-item>
|
||||||
<template #label>
|
<template #label>
|
||||||
<div class="text-gray-500 text-sm">
|
<div class="text-gray-500 text-sm">
|
||||||
生成视频动作迁移消耗的积分,<span class="text-red-500">单位:积分/秒</span>
|
生成视频动作迁移消耗的积分,<el-tag type="primary">单位:积分/秒</el-tag>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<el-input-number
|
<el-input-number
|
||||||
|
|||||||