optimize jimeng power config

This commit is contained in:
GeekMaster
2025-09-16 20:35:53 +08:00
parent 6e6a496f1b
commit 48203e0d31
7 changed files with 123 additions and 126 deletions

View File

@@ -8,15 +8,7 @@ type JimengConfig struct {
// 火山引擎大模型专用的验证方式 // 火山引擎大模型专用的验证方式
ApiKey string `json:"api_key"` ApiKey string `json:"api_key"`
// 算力配置 // 算力配置
Power JimengPower `json:"power"` Powers map[string]int `json:"powers"`
}
// JimengPower 即梦AI算力配置
type JimengPower struct {
Image int `json:"image"` // 图片生成算力,单位:积分/张
Video int `json:"video"` // 视频生成算力,单位:积分/秒
VirtualHuman int `json:"virtual_human"` // 数字人视频生成算力,单位:积分/秒
ActionTransfer int `json:"action_transfer"` // 视频动作迁移算力,单位:积分/秒
} }
// JMTaskStatus 任务状态 // JMTaskStatus 任务状态

View File

@@ -231,21 +231,15 @@ func (h *AdminJimengHandler) UpdateConfig(c *gin.Context) {
} }
// 验证算力配置 // 验证算力配置
if req.Power.Image <= 0 { if len(req.Powers) == 0 {
resp.ERROR(c, "图片生成算力必须大于0") resp.ERROR(c, "请至少配置一个模型的积分")
return return
} }
if req.Power.Video <= 0 { for key, val := range req.Powers {
resp.ERROR(c, "视频生成算力必须大于0") if val <= 0 {
resp.ERROR(c, fmt.Sprintf("模型 %s 的积分必须大于0", key))
return return
} }
if req.Power.VirtualHuman <= 0 {
resp.ERROR(c, "数字人生成算力必须大于0")
return
}
if req.Power.ActionTransfer <= 0 {
resp.ERROR(c, "视频动作迁移算力必须大于0")
return
} }
// 保存配置 // 保存配置

View File

@@ -39,12 +39,12 @@ func NewJimengHandler(app *core.AppServer, jimengService *jimeng.Service, db *go
// RegisterRoutes 注册路由,新增统一任务接口 // RegisterRoutes 注册路由,新增统一任务接口
func (h *JimengHandler) RegisterRoutes() { func (h *JimengHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/jimeng/") group := h.App.Engine.Group("/api/jimeng/")
group.GET("power-config", h.GetPowerConfig)
// 需要用户授权的接口 // 需要用户授权的接口
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis)) group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
{ {
group.POST("task", h.CreateTask) group.POST("task", h.CreateTask)
group.GET("power-config", h.GetPowerConfig)
group.POST("jobs", h.Jobs) group.POST("jobs", h.Jobs)
group.GET("remove", h.Remove) group.GET("remove", h.Remove)
group.GET("retry", h.Retry) group.GET("retry", h.Retry)
@@ -125,15 +125,31 @@ func (h *JimengHandler) CreateTask(c *gin.Context) {
func (h *JimengHandler) getTaskRemark(req types.JimengTaskRequest, jobId uint) string { func (h *JimengHandler) getTaskRemark(req types.JimengTaskRequest, jobId uint) string {
remark := fmt.Sprintf("即梦任务%s任务ID%d", req.ReqKey, jobId) remark := fmt.Sprintf("即梦任务%s任务ID%d", req.ReqKey, jobId)
perUnit, ok := h.App.SysConfig.Jimeng.Powers[req.ReqKey]
if !ok || perUnit <= 0 {
return remark // Fallback if power not found or invalid
}
switch req.TaskType { switch req.TaskType {
case types.JMTaskTypeImage: case types.JMTaskTypeImage:
remark = fmt.Sprintf("即梦图片生成任务ID%d%d积分/张", jobId, h.App.SysConfig.Jimeng.Power.Image) remark = fmt.Sprintf("即梦图片生成任务ID%d%d积分/张", jobId, perUnit)
case types.JMTaskTypeVideo: case types.JMTaskTypeVideo:
remark = fmt.Sprintf("即梦视频生成任务ID%d%d积分/秒, %d秒", jobId, h.App.SysConfig.Jimeng.Power.Video, req.Power/h.App.SysConfig.Jimeng.Power.Video) seconds := 0
if perUnit > 0 {
seconds = req.Power / perUnit
}
remark = fmt.Sprintf("即梦视频生成任务ID%d%d积分/秒, %d秒", jobId, perUnit, seconds)
case types.JMTaskTypeVirtualHuman: case types.JMTaskTypeVirtualHuman:
remark = fmt.Sprintf("即梦数字人视频生成任务ID%d%d积分/秒, %d秒", jobId, h.App.SysConfig.Jimeng.Power.VirtualHuman, req.Power/h.App.SysConfig.Jimeng.Power.VirtualHuman) seconds := 0
if perUnit > 0 {
seconds = req.Power / perUnit
}
remark = fmt.Sprintf("即梦数字人视频生成任务ID%d%d积分/秒, %d秒", jobId, perUnit, seconds)
case types.JMTaskTypeActionTransfer: case types.JMTaskTypeActionTransfer:
remark = fmt.Sprintf("即梦视频动作迁移任务ID%d%d积分/秒, %d秒", jobId, h.App.SysConfig.Jimeng.Power.ActionTransfer, req.Power/h.App.SysConfig.Jimeng.Power.ActionTransfer) seconds := 0
if perUnit > 0 {
seconds = req.Power / perUnit
}
remark = fmt.Sprintf("即梦视频动作迁移任务ID%d%d积分/秒, %d秒", jobId, perUnit, seconds)
} }
return remark return remark
} }
@@ -299,20 +315,22 @@ func (h *JimengHandler) Retry(c *gin.Context) {
resp.SUCCESS(c, gin.H{"message": "重试任务已提交"}) resp.SUCCESS(c, gin.H{"message": "重试任务已提交"})
} }
// getPowerFromConfig 从配置中获取指定类型的算力消耗
func (h *JimengHandler) getTaskPower(req types.JimengTaskRequest) (int, error) { func (h *JimengHandler) getTaskPower(req types.JimengTaskRequest) (int, error) {
logger.Debugf("getTaskPower req: %+v", req) logger.Debugf("getTaskPower req: %+v", req)
config := h.App.SysConfig.Jimeng config := h.App.SysConfig.Jimeng
basePower, ok := config.Powers[req.ReqKey]
if !ok || basePower <= 0 {
return 0, errors.New("未配置模型积分或配置不合法")
}
switch req.TaskType { switch req.TaskType {
case types.JMTaskTypeImage: case types.JMTaskTypeImage:
return config.Power.Image, nil return basePower, nil
case types.JMTaskTypeVideo: case types.JMTaskTypeVideo:
if req.Duration == 0 { if req.Duration == 0 {
return 0, errors.New("视频时长不能为0") return 0, errors.New("视频时长不能为0")
} }
return config.Power.Video * req.Duration, nil return basePower * req.Duration, nil
case types.JMTaskTypeVirtualHuman: case types.JMTaskTypeVirtualHuman:
// TODO 计算音频时长
if req.AudioURL == "" { if req.AudioURL == "" {
return 0, errors.New("音频URL不能为空") return 0, errors.New("音频URL不能为空")
} }
@@ -320,9 +338,12 @@ func (h *JimengHandler) getTaskPower(req types.JimengTaskRequest) (int, error) {
if err != nil { if err != nil {
return 0, err return 0, err
} }
return config.Power.VirtualHuman * int(audioDuration.Seconds()), nil seconds := int(audioDuration.Seconds())
if seconds <= 0 {
return 0, errors.New("音频时长无效")
}
return basePower * seconds, nil
case types.JMTaskTypeActionTransfer: case types.JMTaskTypeActionTransfer:
// TODO 计算视频时长
if req.VideoURL == "" { if req.VideoURL == "" {
return 0, errors.New("视频URL不能为空") return 0, errors.New("视频URL不能为空")
} }
@@ -330,7 +351,11 @@ func (h *JimengHandler) getTaskPower(req types.JimengTaskRequest) (int, error) {
if err != nil { if err != nil {
return 0, err return 0, err
} }
return config.Power.ActionTransfer * int(videoDuration.Seconds()), nil seconds := int(videoDuration.Seconds())
if seconds <= 0 {
return 0, errors.New("视频时长无效")
}
return basePower * seconds, nil
default: default:
return 0, errors.New("任务类型不支持") return 0, errors.New("任务类型不支持")
} }
@@ -340,9 +365,6 @@ func (h *JimengHandler) getTaskPower(req types.JimengTaskRequest) (int, error) {
func (h *JimengHandler) GetPowerConfig(c *gin.Context) { func (h *JimengHandler) GetPowerConfig(c *gin.Context) {
config := h.App.SysConfig.Jimeng config := h.App.SysConfig.Jimeng
resp.SUCCESS(c, gin.H{ resp.SUCCESS(c, gin.H{
"image": config.Power.Image, "powers": config.Powers,
"video": config.Power.Video,
"virtual_human": config.Power.VirtualHuman,
"action_transfer": config.Power.ActionTransfer,
}) })
} }

View File

@@ -341,6 +341,7 @@
// 提示词指南样式 // 提示词指南样式
.prompt-guide { .prompt-guide {
margin: 12px 0 16px; margin: 12px 0 16px;
background-color: var(--el-fill-color-blank);
.guide-title { .guide-title {
display: flex; display: flex;

View File

@@ -36,7 +36,7 @@ export const useJimengStore = defineStore('jimeng', () => {
const shareStore = useSharedStore() const shareStore = useSharedStore()
// 积分消耗配置 // 积分消耗配置
const powerConfig = reactive({}) const powerConfig = reactive({ powers: {} })
const currentPowerCost = ref('0积分') const currentPowerCost = ref('0积分')
// 功能配置 // 功能配置
@@ -83,12 +83,10 @@ export const useJimengStore = defineStore('jimeng', () => {
// 获取状态类型 // 获取状态类型
const getTaskType = (type) => { const getTaskType = (type) => {
const typeMap = { const typeMap = {
text_to_image: 'primary', image: 'info',
image_to_image: 'primary', video: 'primary',
image_edit: 'primary', virtual_human: 'success',
image_effects: 'primary', action_transfer: 'warning',
text_to_video: 'success',
image_to_video: 'success',
} }
return typeMap[type] || 'primary' return typeMap[type] || 'primary'
} }
@@ -124,7 +122,7 @@ export const useJimengStore = defineStore('jimeng', () => {
} }
total.value = data.total || 0 total.value = data.total || 0
if (data.items.length < pageSize.value) { if (!data.items || data.items.length < pageSize.value) {
isOver.value = true isOver.value = true
} }
if (pageNum === 1) { if (pageNum === 1) {
@@ -150,7 +148,7 @@ export const useJimengStore = defineStore('jimeng', () => {
page_size: 20, page_size: 20,
}) })
const data = response.data const data = response.data
if (data.items.length === 0) { if (!data.items || data.items.length === 0) {
stopPolling() stopPolling()
return return
} }
@@ -184,7 +182,6 @@ export const useJimengStore = defineStore('jimeng', () => {
shareStore.setShowLoginDialog(true) shareStore.setShowLoginDialog(true)
return return
} }
console.log(formData.value)
for (const key in requiredKeys.value) { for (const key in requiredKeys.value) {
if (!formData.value[key]) { if (!formData.value[key]) {
showMessageError('缺少参数:' + requiredKeys.value[key].label) showMessageError('缺少参数:' + requiredKeys.value[key].label)
@@ -284,20 +281,32 @@ export const useJimengStore = defineStore('jimeng', () => {
} }
const setFunctionPowers = () => { const setFunctionPowers = () => {
if (activeFunction.value === 'image') { nextTick(() => {
currentPowerCost.value = `${powerConfig.image}积分/张` const key = formData.value.req_key
} else { const perUnit = key ? powerConfig.powers[key] : 0
currentPowerCost.value = `${powerConfig.video}积分/秒` if (!perUnit) {
currentPowerCost.value = '未配置积分'
return
} }
currentPowerCost.value =
activeFunction.value === 'image' ? `${perUnit}积分/张` : `${perUnit}积分/秒`
})
} }
watch(
() => formData.value,
() => {
setFunctionPowers()
}
)
// 初始化方法 // 初始化方法
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) powerConfig.powers = powerRes.data.powers || {}
setFunctionPowers() setFunctionPowers()
} }
const user = await checkSession() const user = await checkSession()

View File

@@ -19,7 +19,7 @@
</div> </div>
<!-- 提示词编写指南可折叠 --> <!-- 提示词编写指南可折叠 -->
<div class="prompt-guide"> <div class="prompt-guide pl-2">
<el-collapse v-model="guideActive"> <el-collapse v-model="guideActive">
<el-collapse-item name="guide"> <el-collapse-item name="guide">
<template #title> <template #title>

View File

@@ -82,59 +82,44 @@
<el-divider /> <el-divider />
<!-- 算力配置分组 --> <!-- 算力配置分组 -->
<div class="mb-3"> <div class="mb-3">
<h3 class="heading-3 mb-3">算力配置</h3> <h3 class="heading-3 mb-3">任务积分配置</h3>
<el-form-item> <Alert type="info" class="mb-3">
<template #label> <div class="text-gray-500">
<div class="text-gray-500 text-sm"> 图片类模型统一都是 0.2 元一张假如你100积分售价1元建议设置20积分/
生成图片消耗的积分包括文生图图生图图片编辑图片特效<el-tag type="primary" </div>
>单位积分/</el-tag <div class="text-gray-500">
视频/数字人/动作迁移单位积分/但是不同的模型的价格不一样建议去火山方舟控制台查看根据价格设置积分
</div>
</Alert>
<div v-for="func in functions" :key="func.key" class="mb-4">
<h4 class="mb-2 text-base font-bold flex items-center gap-2">
<i class="iconfont" :class="func.icon"></i>
{{ func.name }}
<el-tag size="small" type="info">{{ getUnit(func.key) }}</el-tag>
</h4>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<div
v-for="model in params[func.key]"
:key="model.key"
class="p-3 rounded-md border border-gray-100"
> >
<div class="text-sm mb-2">
<div class="font-bold">{{ model.name }}</div>
<div class="text-gray-500 line-clamp-2" :title="model.label">
{{ model.label }}
</div> </div>
</template>
<el-input-number
v-model="jimengConfig.power.image"
:min="1"
placeholder="请输入图片生成算力消耗"
/>
</el-form-item>
<el-form-item>
<template #label>
<div class="text-gray-500 text-sm">
生成视频消耗的积分包括文生视频图生视频<el-tag type="primary"
>单位积分/</el-tag
>
</div> </div>
</template>
<el-input-number <el-input-number
v-model="jimengConfig.power.video" v-model="jimengConfig.powers[model.key]"
:min="1" :min="1"
placeholder="请输入视频生成算力消耗" :placeholder="`对应模型:${model.key}${getUnit(func.key)}`"
class="w-full"
/> />
</el-form-item> <div class="text-xs text-gray-400 mt-1">对应模型{{ model.key }}</div>
<el-form-item> </div>
<template #label>
<div class="text-gray-500 text-sm">
生成数字人视频消耗的积分<el-tag type="primary">单位积分/</el-tag>
</div> </div>
</template>
<el-input-number
v-model="jimengConfig.power.virtual_human"
:min="1"
placeholder="请输入数字人视频生成算力消耗"
/>
</el-form-item>
<el-form-item>
<template #label>
<div class="text-gray-500 text-sm">
生成视频动作迁移消耗的积分<el-tag type="primary">单位积分/</el-tag>
</div> </div>
</template>
<el-input-number
v-model="jimengConfig.power.action_transfer"
:min="1"
placeholder="请输入视频动作迁移算力消耗"
/>
</el-form-item>
</div> </div>
<div style="padding: 10px"> <div style="padding: 10px">
<el-form-item> <el-form-item>
@@ -149,6 +134,7 @@
<script setup> <script setup>
import Alert from '@/components/ui/Alert.vue' import Alert from '@/components/ui/Alert.vue'
import { JimengFunctions, JimengParams } from '@/store/data/jimeng_params'
import { httpGet, httpPost } from '@/utils/http' import { httpGet, httpPost } from '@/utils/http'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { onMounted, ref } from 'vue' import { onMounted, ref } from 'vue'
@@ -156,20 +142,15 @@ import { onMounted, ref } from 'vue'
const jimengConfig = ref({ const jimengConfig = ref({
access_key: '', access_key: '',
secret_key: '', secret_key: '',
power: { api_key: '',
text_to_image: 10, powers: {},
image_to_image: 15,
image_edit: 20,
image_effects: 25,
text_to_video: 30,
image_to_video: 35,
},
}) })
const loading = ref(true) const loading = ref(true)
const saving = ref(false) const saving = ref(false)
const testing = ref(false)
const configFormRef = ref() const configFormRef = ref()
const functions = JimengFunctions
const params = JimengParams
// 表单验证规则 // 表单验证规则
const rules = { const rules = {
@@ -177,6 +158,8 @@ const rules = {
secret_key: [{ required: true, message: '请输入SecretKey', trigger: 'blur' }], secret_key: [{ required: true, message: '请输入SecretKey', trigger: 'blur' }],
} }
const getUnit = (funcKey) => (funcKey === 'image' ? '积分/张' : '积分/秒')
onMounted(() => { onMounted(() => {
loadConfig() loadConfig()
}) })
@@ -185,7 +168,9 @@ onMounted(() => {
const loadConfig = async () => { const loadConfig = async () => {
try { try {
const res = await httpGet('/api/admin/config/get?key=jimeng') const res = await httpGet('/api/admin/config/get?key=jimeng')
jimengConfig.value = res.data const cfg = res.data || {}
cfg.powers = cfg.powers || {}
jimengConfig.value = cfg
} catch (e) { } catch (e) {
ElMessage.error('加载配置失败: ' + e.message) ElMessage.error('加载配置失败: ' + e.message)
} finally { } finally {
@@ -214,14 +199,8 @@ const resetConfig = () => {
jimengConfig.value = { jimengConfig.value = {
access_key: '', access_key: '',
secret_key: '', secret_key: '',
power: { api_key: '',
text_to_image: 10, powers: {},
image_to_image: 15,
image_edit: 20,
image_effects: 25,
text_to_video: 30,
image_to_video: 35,
},
} }
ElMessage.info('配置已重置') ElMessage.info('配置已重置')
} }
@@ -237,7 +216,7 @@ const resetConfig = () => {
.container { .container {
width: 100%; width: 100%;
max-width: 800px; max-width: 1000px;
} }
.heading-3 { .heading-3 {