1
This commit is contained in:
Binary file not shown.
@@ -17,4 +17,5 @@ const (
|
||||
TableNamePaymentChannelTrade = "payment_channel_trade"
|
||||
TableNamePaymentConfig = "payment_config"
|
||||
TableNameRegionPricing = "region_pricing"
|
||||
TableNameUserModelConfig = "user_model_config"
|
||||
)
|
||||
|
||||
@@ -2,22 +2,26 @@ package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"video-factory/shortdrama/middleware"
|
||||
"video-factory/shortdrama/model/dto"
|
||||
"video-factory/shortdrama/model/entity"
|
||||
"video-factory/shortdrama/service"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type config struct{}
|
||||
|
||||
var Config = new(config)
|
||||
|
||||
func (c *config) Get(ctx context.Context, req *dto.GetModelConfigReq) (res *dto.GetModelConfigRes, err error) {
|
||||
return service.ConfigService.GetResponse(ctx), nil
|
||||
func (c *config) GetModelList(ctx context.Context, req *dto.GetModelConfigListReq) (res *dto.GetModelConfigListRes, err error) {
|
||||
return service.ConfigService.GetModelListResponse(ctx), nil
|
||||
}
|
||||
|
||||
func (c *config) Save(ctx context.Context, req *dto.SaveModelConfigReq) (res *struct{}, err error) {
|
||||
return nil, service.ConfigService.Save(ctx, req)
|
||||
func (c *config) SaveModelConfig(ctx context.Context, req *dto.SaveModelConfigReq) (res *struct{}, err error) {
|
||||
return nil, service.ConfigService.SaveModelConfig(ctx, req)
|
||||
}
|
||||
|
||||
func (c *config) GetPayment(ctx context.Context, req *dto.GetPaymentConfigReq) (res *dto.GetPaymentConfigRes, err error) {
|
||||
@@ -39,8 +43,6 @@ func (c *config) GetPayment(ctx context.Context, req *dto.GetPaymentConfigReq) (
|
||||
res.Wechat.H5 = cfg
|
||||
case "app":
|
||||
res.Wechat.App = cfg
|
||||
case "native":
|
||||
res.Wechat.Native = cfg
|
||||
default:
|
||||
res.Wechat.Jsapi = cfg
|
||||
}
|
||||
@@ -67,6 +69,38 @@ func (c *config) GetPayment(ctx context.Context, req *dto.GetPaymentConfigReq) (
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *config) GetUserConfig(ctx context.Context, req *dto.GetUserModelConfigReq) (res *dto.GetUserModelConfigRes, err error) {
|
||||
r := g.RequestFromCtx(ctx)
|
||||
userId := middleware.GetUserId(r)
|
||||
if userId <= 0 {
|
||||
return nil, fmt.Errorf("未登录")
|
||||
}
|
||||
cfg := service.ConfigService.GetUserConfig(ctx, userId)
|
||||
return &dto.GetUserModelConfigRes{UserModelConfig: cfg}, nil
|
||||
}
|
||||
|
||||
func (c *config) SaveUserConfig(ctx context.Context, req *dto.SaveUserModelConfigReq) (res *struct{}, err error) {
|
||||
r := g.RequestFromCtx(ctx)
|
||||
userId := middleware.GetUserId(r)
|
||||
if userId <= 0 {
|
||||
return nil, fmt.Errorf("未登录")
|
||||
}
|
||||
cfg := &entity.UserModelConfig{
|
||||
ModelConfigId: req.ModelConfigId,
|
||||
ApiKey: req.ApiKey,
|
||||
Temperature: req.Temperature,
|
||||
MaxTokens: req.MaxTokens,
|
||||
IsActive: req.IsActive,
|
||||
}
|
||||
return nil, service.ConfigService.SaveUserConfig(ctx, userId, cfg)
|
||||
}
|
||||
|
||||
func (c *config) GetUserModelList(ctx context.Context, req *dto.GetUserModelListReq) (res *dto.GetUserModelListRes, err error) {
|
||||
r := g.RequestFromCtx(ctx)
|
||||
userId := middleware.GetUserId(r)
|
||||
return service.ConfigService.GetUserModelList(ctx, userId), nil
|
||||
}
|
||||
|
||||
func (c *config) SavePayment(ctx context.Context, req *dto.SavePaymentConfigReq) (res *struct{}, err error) {
|
||||
return nil, service.ConfigService.SavePaymentConfig(ctx, &entity.PaymentConfig{
|
||||
Channel: req.Channel,
|
||||
|
||||
@@ -16,86 +16,120 @@ type modelConfigDao struct{}
|
||||
|
||||
func init() {
|
||||
ctx := context.Background()
|
||||
if _, err := g.DB().Exec(ctx, `CREATE TABLE IF NOT EXISTS `+public.TableNameModelConfig+` (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
chat_api_key TEXT NOT NULL DEFAULT '',
|
||||
chat_base_url TEXT NOT NULL DEFAULT '',
|
||||
chat_model_name TEXT NOT NULL DEFAULT '',
|
||||
max_tokens INTEGER NOT NULL DEFAULT 4096,
|
||||
temperature REAL NOT NULL DEFAULT 0.8,
|
||||
chat_schema TEXT NOT NULL DEFAULT '',
|
||||
video_api_key TEXT NOT NULL DEFAULT '',
|
||||
video_base_url TEXT NOT NULL DEFAULT '',
|
||||
video_model_name TEXT NOT NULL DEFAULT '',
|
||||
video_schema TEXT NOT NULL DEFAULT '',
|
||||
video_task_callback_url TEXT NOT NULL DEFAULT '',
|
||||
price_per_second INTEGER NOT NULL DEFAULT 0,
|
||||
created_at DATETIME DEFAULT (datetime('now','localtime')),
|
||||
updated_at DATETIME DEFAULT (datetime('now','localtime'))
|
||||
)`); err != nil {
|
||||
_, err := g.DB().Exec(ctx,
|
||||
"CREATE TABLE IF NOT EXISTS "+public.TableNameModelConfig+" ("+
|
||||
"id INTEGER PRIMARY KEY AUTOINCREMENT,"+
|
||||
"model_type TEXT NOT NULL DEFAULT '',"+
|
||||
"model_name TEXT NOT NULL DEFAULT '',"+
|
||||
"base_url TEXT NOT NULL DEFAULT '',"+
|
||||
"schema TEXT NOT NULL DEFAULT '',"+
|
||||
"price INTEGER NOT NULL DEFAULT 0,"+
|
||||
"price_unit TEXT NOT NULL DEFAULT 'second',"+
|
||||
"task_callback_url TEXT NOT NULL DEFAULT '',"+
|
||||
"created_at DATETIME DEFAULT (datetime('now','localtime')),"+
|
||||
"updated_at DATETIME DEFAULT (datetime('now','localtime'))"+
|
||||
")")
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "创建模型配置表失败: %v", err)
|
||||
}
|
||||
|
||||
// 清理旧字段
|
||||
for _, col := range []string{
|
||||
"max_single_duration", "min_single_duration",
|
||||
"chat_provider", "video_provider",
|
||||
"video_no_duration_support",
|
||||
"video_model_category", "image_model_name",
|
||||
"chat_params", "video_params",
|
||||
"max_reference_images",
|
||||
"max_ref_video_count", "max_ref_video_file_size", "max_ref_video_duration", "ref_video_formats",
|
||||
"max_ref_audio_count", "max_ref_audio_file_size", "max_ref_audio_duration", "ref_audio_formats",
|
||||
"max_ref_image_file_size", "ref_image_formats",
|
||||
"video_query_url",
|
||||
"payment_config",
|
||||
} {
|
||||
if _, err := g.DB().Exec(ctx, `ALTER TABLE `+public.TableNameModelConfig+` DROP COLUMN `+col); err != nil {
|
||||
g.Log().Debugf(ctx, "删除孤儿列 %s 失败(可能已删除): %v", col, err)
|
||||
// 检测旧表结构:含 api_key / chat_api_key / is_active 等旧列 → 迁移
|
||||
hasOldColumns := false
|
||||
if r, _ := g.DB().GetAll(ctx, "PRAGMA table_info("+public.TableNameModelConfig+")"); r != nil {
|
||||
for _, col := range r {
|
||||
name := col["name"].String()
|
||||
if name == "api_key" || name == "chat_api_key" || name == "is_active" || name == "price_per_second" || name == "max_tokens" {
|
||||
hasOldColumns = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
// 补充新字段
|
||||
for _, col := range []string{
|
||||
"chat_schema",
|
||||
"video_schema",
|
||||
"video_task_callback_url",
|
||||
"price_per_second",
|
||||
} {
|
||||
if _, err := g.DB().Exec(ctx, `ALTER TABLE `+public.TableNameModelConfig+` ADD COLUMN `+col+` TEXT NOT NULL DEFAULT ''`); err != nil {
|
||||
g.Log().Debugf(ctx, "添加列 %s 失败(可能已存在): %v", col, err)
|
||||
|
||||
if hasOldColumns {
|
||||
g.Log().Info(ctx, "检测到旧表结构,开始迁移 model_config...")
|
||||
newTable := public.TableNameModelConfig + "_new"
|
||||
_, _ = g.DB().Exec(ctx,
|
||||
"CREATE TABLE IF NOT EXISTS "+newTable+" ("+
|
||||
"id INTEGER PRIMARY KEY AUTOINCREMENT,"+
|
||||
"model_type TEXT NOT NULL DEFAULT '',"+
|
||||
"model_name TEXT NOT NULL DEFAULT '',"+
|
||||
"base_url TEXT NOT NULL DEFAULT '',"+
|
||||
"schema TEXT NOT NULL DEFAULT '',"+
|
||||
"price INTEGER NOT NULL DEFAULT 0,"+
|
||||
"price_unit TEXT NOT NULL DEFAULT 'second',"+
|
||||
"task_callback_url TEXT NOT NULL DEFAULT '',"+
|
||||
"created_at DATETIME DEFAULT (datetime('now','localtime')),"+
|
||||
"updated_at DATETIME DEFAULT (datetime('now','localtime'))"+
|
||||
")")
|
||||
|
||||
hasChatApiKey := false
|
||||
if _, err := g.DB().Exec(ctx, "SELECT chat_api_key FROM "+public.TableNameModelConfig+" LIMIT 1"); err == nil {
|
||||
hasChatApiKey = true
|
||||
}
|
||||
}
|
||||
// price_per_second 是 INTEGER,补一个独立的 ALTER
|
||||
if _, err := g.DB().Exec(ctx, `ALTER TABLE `+public.TableNameModelConfig+` ADD COLUMN price_per_second INTEGER NOT NULL DEFAULT 0`); err != nil {
|
||||
g.Log().Debugf(ctx, "添加列 price_per_second 失败(可能已存在): %v", err)
|
||||
now := gtime.Now().Format("Y-m-d H:i:s")
|
||||
|
||||
if hasChatApiKey {
|
||||
oldRow, _ := g.DB().Model(public.TableNameModelConfig).Ctx(ctx).Limit(1).One()
|
||||
if oldRow != nil && !oldRow.IsEmpty() {
|
||||
_, _ = g.DB().Model(newTable).Ctx(ctx).Data(g.Map{
|
||||
"model_type": "chat", "model_name": oldRow["chat_model_name"],
|
||||
"base_url": oldRow["chat_base_url"],
|
||||
"schema": oldRow["chat_schema"],
|
||||
"price": 0, "price_unit": "video",
|
||||
"created_at": now, "updated_at": now,
|
||||
}).Insert()
|
||||
_, _ = g.DB().Model(newTable).Ctx(ctx).Data(g.Map{
|
||||
"model_type": "video", "model_name": oldRow["video_model_name"],
|
||||
"base_url": oldRow["video_base_url"],
|
||||
"schema": oldRow["video_schema"], "task_callback_url": oldRow["video_task_callback_url"],
|
||||
"price": gconv.Int(oldRow["price_per_second"]), "price_unit": "second",
|
||||
"created_at": now, "updated_at": now,
|
||||
}).Insert()
|
||||
}
|
||||
} else {
|
||||
oldRows, _ := g.DB().Model(public.TableNameModelConfig).Ctx(ctx).OrderAsc("id").All()
|
||||
for _, or := range oldRows {
|
||||
mt := or["model_type"].String()
|
||||
priceVal := gconv.Int(or["price_per_second"])
|
||||
if priceVal <= 0 {
|
||||
priceVal = gconv.Int(or["price"])
|
||||
}
|
||||
unit := "second"
|
||||
if mt == "chat" {
|
||||
unit = "video"
|
||||
}
|
||||
_, _ = g.DB().Model(newTable).Ctx(ctx).Data(g.Map{
|
||||
"model_type": mt, "model_name": or["model_name"],
|
||||
"base_url": or["base_url"],
|
||||
"schema": or["schema"],
|
||||
"price": priceVal, "price_unit": unit,
|
||||
"task_callback_url": or["task_callback_url"],
|
||||
"created_at": now, "updated_at": now,
|
||||
}).Insert()
|
||||
}
|
||||
}
|
||||
_, _ = g.DB().Exec(ctx, "DROP TABLE "+public.TableNameModelConfig)
|
||||
_, _ = g.DB().Exec(ctx, "ALTER TABLE "+newTable+" RENAME TO "+public.TableNameModelConfig)
|
||||
g.Log().Info(ctx, "model_config 表结构迁移完成")
|
||||
}
|
||||
|
||||
// 初始化默认模型配置(仅当表为空时)
|
||||
count, _ := g.DB().Model(public.TableNameModelConfig).Ctx(ctx).Count()
|
||||
if count == 0 {
|
||||
_, err := g.DB().Model(public.TableNameModelConfig).Ctx(ctx).Data(g.Map{
|
||||
"chat_api_key": "",
|
||||
"chat_base_url": "",
|
||||
"chat_model_name": "",
|
||||
"max_tokens": 4096,
|
||||
"temperature": 0.85,
|
||||
"video_api_key": "",
|
||||
"video_base_url": "",
|
||||
"video_task_callback_url": "",
|
||||
"video_model_name": "",
|
||||
"price_per_second": 10,
|
||||
"created_at": gtime.Now().Format("Y-m-d H:i:s"),
|
||||
"updated_at": gtime.Now().Format("Y-m-d H:i:s"),
|
||||
}).Insert()
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "初始化默认模型配置失败: %v", err)
|
||||
} else {
|
||||
g.Log().Info(ctx, "已初始化默认模型配置")
|
||||
now := gtime.Now().Format("Y-m-d H:i:s")
|
||||
defaults := []g.Map{
|
||||
{"model_type": "chat", "model_name": "", "base_url": "", "schema": "{\"properties\":{\"parameters\":{\"properties\":{\"max_tokens\":{\"type\":\"integer\",\"description\":\"最大Token数\",\"default\":4096,\"maximum\":65535}}}}}}", "price": 0, "price_unit": "video", "created_at": now, "updated_at": now},
|
||||
{"model_type": "video", "model_name": "", "base_url": "", "schema": "", "price": 10, "price_unit": "second", "created_at": now, "updated_at": now},
|
||||
}
|
||||
for _, m := range defaults {
|
||||
if _, e := g.DB().Model(public.TableNameModelConfig).Ctx(ctx).Data(m).Insert(); e != nil {
|
||||
g.Log().Warningf(ctx, "初始化默认模型配置失败: %v", e)
|
||||
}
|
||||
}
|
||||
g.Log().Info(ctx, "已初始化默认模型配置(chat + video)")
|
||||
}
|
||||
}
|
||||
|
||||
// GetFirst 获取第一条配置行
|
||||
func (d *modelConfigDao) GetFirst(ctx context.Context) (res *entity.ModelConfig, err error) {
|
||||
r, err := g.DB().Model(public.TableNameModelConfig).Ctx(ctx).OrderAsc("id").Limit(1).One()
|
||||
if err != nil {
|
||||
@@ -109,34 +143,47 @@ func (d *modelConfigDao) GetFirst(ctx context.Context) (res *entity.ModelConfig,
|
||||
return
|
||||
}
|
||||
|
||||
// Save 保存配置:存在则更新,不存在则插入
|
||||
func (d *modelConfigDao) Save(ctx context.Context, data *entity.ModelConfig) error {
|
||||
existing, err := d.GetFirst(ctx)
|
||||
func (d *modelConfigDao) GetActiveModelByType(ctx context.Context, modelType string) (res *entity.ModelConfig, err error) {
|
||||
r, err := g.DB().Model(public.TableNameModelConfig).Ctx(ctx).
|
||||
Where("model_type", modelType).Limit(1).One()
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
if existing != nil {
|
||||
data.Id = existing.Id
|
||||
_, err = g.DB().Model(public.TableNameModelConfig).Ctx(ctx).Data(data).Where("id", existing.Id).Update()
|
||||
return err
|
||||
if r == nil {
|
||||
return nil, nil
|
||||
}
|
||||
m := gconv.Map(data, gconv.MapOption{Tags: []string{"orm"}})
|
||||
delete(m, "id")
|
||||
m["created_at"] = gtime.Now().Format("Y-m-d H:i:s")
|
||||
m["updated_at"] = gtime.Now().Format("Y-m-d H:i:s")
|
||||
_, err = g.DB().Model(public.TableNameModelConfig).Ctx(ctx).Data(m).Insert()
|
||||
return err
|
||||
res = new(entity.ModelConfig)
|
||||
err = r.Struct(&res)
|
||||
return
|
||||
}
|
||||
|
||||
// UpdateField 更新指定字段
|
||||
func (d *modelConfigDao) UpdateField(ctx context.Context, field string, value interface{}) error {
|
||||
existing, err := d.GetFirst(ctx)
|
||||
func (d *modelConfigDao) GetAll(ctx context.Context) (res []*entity.ModelConfig, err error) {
|
||||
r, err := g.DB().Model(public.TableNameModelConfig).Ctx(ctx).OrderAsc("id").All()
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
if existing == nil {
|
||||
return nil
|
||||
res = make([]*entity.ModelConfig, 0)
|
||||
if r != nil {
|
||||
err = r.Structs(&res)
|
||||
}
|
||||
_, err = g.DB().Model(public.TableNameModelConfig).Ctx(ctx).Data(g.Map{field: value}).Where("id", existing.Id).Update()
|
||||
return err
|
||||
return
|
||||
}
|
||||
|
||||
func (d *modelConfigDao) Save(ctx context.Context, data *entity.ModelConfig) error {
|
||||
if data.Id > 0 {
|
||||
_, err := g.DB().Model(public.TableNameModelConfig).Ctx(ctx).Data(data).Where("id", data.Id).Update()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
m := gconv.Map(data, gconv.MapOption{Tags: []string{"orm"}})
|
||||
delete(m, "id")
|
||||
m["created_at"] = gtime.Now().Format("Y-m-d H:i:s")
|
||||
m["updated_at"] = gtime.Now().Format("Y-m-d H:i:s")
|
||||
_, err := g.DB().Model(public.TableNameModelConfig).Ctx(ctx).Data(m).Insert()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"video-factory/shortdrama/consts/public"
|
||||
"video-factory/shortdrama/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
var UserModelConfig = &userModelConfigDao{}
|
||||
|
||||
type userModelConfigDao struct{}
|
||||
|
||||
func init() {
|
||||
ctx := context.Background()
|
||||
// 检测表是否已存在
|
||||
var tableExists bool
|
||||
if r, _ := g.DB().Exec(ctx, "SELECT count(*) FROM sqlite_master WHERE type='table' AND name='"+public.TableNameUserModelConfig+"'"); r != nil {
|
||||
tableExists = true
|
||||
}
|
||||
|
||||
if !tableExists {
|
||||
_, err := g.DB().Exec(ctx, `CREATE TABLE `+public.TableNameUserModelConfig+` (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL DEFAULT 0,
|
||||
model_config_id INTEGER NOT NULL DEFAULT 0,
|
||||
api_key TEXT NOT NULL DEFAULT '',
|
||||
temperature REAL NOT NULL DEFAULT 0,
|
||||
max_tokens INTEGER NOT NULL DEFAULT 0,
|
||||
is_active INTEGER NOT NULL DEFAULT 0,
|
||||
created_at DATETIME DEFAULT (datetime('now','localtime')),
|
||||
updated_at DATETIME DEFAULT (datetime('now','localtime'))
|
||||
)`)
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "创建用户模型配置表失败: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 表已存在,检测列类型是否完整
|
||||
r, _ := g.DB().GetAll(ctx, "PRAGMA table_info("+public.TableNameUserModelConfig+")")
|
||||
var needsMigrate bool
|
||||
for _, col := range r {
|
||||
name := col["name"].String()
|
||||
if name == "api_key" && col["notnull"].Int() == 0 {
|
||||
needsMigrate = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if needsMigrate {
|
||||
g.Log().Info(ctx, "检测到 user_model_config 列类型异常,开始迁移...")
|
||||
newTable := public.TableNameUserModelConfig + "_new"
|
||||
_, _ = g.DB().Exec(ctx, `CREATE TABLE `+newTable+` (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL DEFAULT 0,
|
||||
model_config_id INTEGER NOT NULL DEFAULT 0,
|
||||
api_key TEXT NOT NULL DEFAULT '',
|
||||
temperature REAL NOT NULL DEFAULT 0,
|
||||
max_tokens INTEGER NOT NULL DEFAULT 0,
|
||||
is_active INTEGER NOT NULL DEFAULT 0,
|
||||
created_at DATETIME DEFAULT (datetime('now','localtime')),
|
||||
updated_at DATETIME DEFAULT (datetime('now','localtime'))
|
||||
)`)
|
||||
_, _ = g.DB().Exec(ctx,
|
||||
`INSERT INTO `+newTable+`(user_id,model_config_id,api_key,temperature,max_tokens,is_active,created_at,updated_at)
|
||||
SELECT user_id,COALESCE(model_config_id,0),
|
||||
COALESCE(api_key,''),
|
||||
COALESCE(temperature,0),
|
||||
COALESCE(max_tokens,0),
|
||||
COALESCE(is_active,0),
|
||||
COALESCE(created_at,datetime('now','localtime')),
|
||||
COALESCE(updated_at,datetime('now','localtime'))
|
||||
FROM `+public.TableNameUserModelConfig)
|
||||
_, _ = g.DB().Exec(ctx, "DROP TABLE "+public.TableNameUserModelConfig)
|
||||
_, _ = g.DB().Exec(ctx, "ALTER TABLE "+newTable+" RENAME TO "+public.TableNameUserModelConfig)
|
||||
g.Log().Info(ctx, "user_model_config 表结构迁移完成")
|
||||
}
|
||||
}
|
||||
|
||||
// GetActiveByUserId 获取指定用户当前使用中的模型配置
|
||||
func (d *userModelConfigDao) GetActiveByUserId(ctx context.Context, userId int64) (res *entity.UserModelConfig, err error) {
|
||||
r, err := g.DB().Model(public.TableNameUserModelConfig).Ctx(ctx).
|
||||
Where("user_id", userId).Where("is_active", 1).Limit(1).One()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if r == nil {
|
||||
return nil, nil
|
||||
}
|
||||
res = new(entity.UserModelConfig)
|
||||
err = r.Struct(&res)
|
||||
return
|
||||
}
|
||||
|
||||
// GetByUserId 获取指定用户所有模型配置(降序排列)
|
||||
func (d *userModelConfigDao) GetByUserId(ctx context.Context, userId int64) (res []*entity.UserModelConfig, err error) {
|
||||
r, err := g.DB().Model(public.TableNameUserModelConfig).Ctx(ctx).
|
||||
Where("user_id", userId).OrderDesc("id").All()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if r == nil {
|
||||
return nil, nil
|
||||
}
|
||||
res = make([]*entity.UserModelConfig, 0)
|
||||
err = r.Structs(&res)
|
||||
return
|
||||
}
|
||||
|
||||
// GetByModelConfigId 获取指定用户对指定系统模型的配置
|
||||
func (d *userModelConfigDao) GetByModelConfigId(ctx context.Context, userId, modelConfigId int64) (res *entity.UserModelConfig, err error) {
|
||||
r, err := g.DB().Model(public.TableNameUserModelConfig).Ctx(ctx).
|
||||
Where("user_id", userId).Where("model_config_id", modelConfigId).Limit(1).One()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if r == nil {
|
||||
return nil, nil
|
||||
}
|
||||
res = new(entity.UserModelConfig)
|
||||
err = r.Struct(&res)
|
||||
return
|
||||
}
|
||||
|
||||
// Save 保存用户模型配置:存在则更新,不存在则插入
|
||||
// 如果标记为 is_active=1,会自动将同用户同模型配置的其他记录 is_active 置为 0
|
||||
func (d *userModelConfigDao) Save(ctx context.Context, data *entity.UserModelConfig) error {
|
||||
if data.Id > 0 {
|
||||
data.UpdatedAt = gtime.Now()
|
||||
_, err := g.DB().Model(public.TableNameUserModelConfig).Ctx(ctx).Data(data).Where("id", data.Id).Update()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
m := gconv.Map(data, gconv.MapOption{Tags: []string{"orm"}})
|
||||
delete(m, "id")
|
||||
m["created_at"] = gtime.Now().Format("Y-m-d H:i:s")
|
||||
m["updated_at"] = gtime.Now().Format("Y-m-d H:i:s")
|
||||
r, err := g.DB().Model(public.TableNameUserModelConfig).Ctx(ctx).Data(m).Insert()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 获取新插入记录的 ID,供后续 is_active 清理使用
|
||||
if lid, _ := r.LastInsertId(); lid > 0 {
|
||||
data.Id = lid
|
||||
}
|
||||
}
|
||||
// 如果标记为使用中,将同用户其他同模型类型的配置设为非使用中
|
||||
if data.IsActive == 1 && data.UserId > 0 && data.ModelConfigId > 0 {
|
||||
_, _ = g.DB().Model(public.TableNameUserModelConfig).Ctx(ctx).
|
||||
Where("user_id", data.UserId).Where("model_config_id", data.ModelConfigId).
|
||||
Where("id != ?", data.Id).
|
||||
Data(g.Map{"is_active": 0, "updated_at": gtime.Now().Format("Y-m-d H:i:s")}).Update()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -7,33 +7,65 @@ import (
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type GetModelConfigReq struct {
|
||||
g.Meta `path:"/model" method:"get" tags:"模型配置" summary:"获取模型配置"`
|
||||
*entity.ModelConfig
|
||||
ChatSchema *gjson.Json `json:"chatSchema"`
|
||||
VideoSchema *gjson.Json `json:"videoSchema"`
|
||||
// GetModelConfigListReq 获取所有模型配置列表
|
||||
type GetModelConfigListReq struct {
|
||||
g.Meta `path:"/model-list" method:"get" tags:"模型配置" summary:"获取模型配置列表"`
|
||||
}
|
||||
|
||||
type GetModelConfigRes struct {
|
||||
*entity.ModelConfig
|
||||
ChatSchema *gjson.Json `json:"chatSchema"`
|
||||
VideoSchema *gjson.Json `json:"videoSchema"`
|
||||
type GetModelConfigListRes struct {
|
||||
List []*entity.ModelConfig `json:"list"`
|
||||
}
|
||||
|
||||
// SaveModelConfigReq 保存单个模型配置
|
||||
type SaveModelConfigReq struct {
|
||||
g.Meta `path:"/model" method:"post" tags:"模型配置" summary:"保存模型配置"`
|
||||
ChatApiKey string `v:"required" json:"chatApiKey" dc:"对话模型API密钥"`
|
||||
ChatBaseUrl string `v:"required|url" json:"chatBaseUrl" dc:"对话模型API接口地址"`
|
||||
ChatModelName string `v:"required" json:"chatModelName" dc:"对话模型名称"`
|
||||
MaxTokens int `v:"required" json:"maxTokens" dc:"最大Token数"`
|
||||
Temperature float64 `v:"required" json:"temperature" dc:"温度参数"`
|
||||
ChatSchema *gjson.Json `json:"chatSchema" dc:"对话模型schema"`
|
||||
VideoApiKey string `v:"required" json:"videoApiKey" dc:"视频模型API密钥"`
|
||||
VideoBaseUrl string `v:"required|url" json:"videoBaseUrl" dc:"视频模型API接口地址"`
|
||||
VideoModelName string `v:"required" json:"videoModelName" dc:"视频模型名称"`
|
||||
VideoSchema *gjson.Json `json:"videoSchema" dc:"视频生成模型schema(JSON, 含duration/ref/params/sizes)"`
|
||||
PricePerSecond int `v:"required|min:1" json:"pricePerSecond" dc:"每秒价格(分)"`
|
||||
VideoTaskCallbackUrl string `json:"videoTaskCallbackUrl" dc:"视频生成任务回调地址"`
|
||||
g.Meta `path:"/model" method:"post" tags:"模型配置" summary:"保存模型配置"`
|
||||
Id int64 `json:"id"`
|
||||
ModelType string `json:"modelType" v:"required" dc:"chat=对话模型 video=视频模型"`
|
||||
ModelName string `json:"modelName" v:"required" dc:"模型名称"`
|
||||
BaseUrl string `json:"baseUrl" v:"required" dc:"API接口地址"`
|
||||
Schema *gjson.Json `json:"schema" dc:"请求体JSON Schema"`
|
||||
Price int `json:"price" dc:"价格(分)"`
|
||||
PriceUnit string `json:"priceUnit" dc:"价格单位(second=每秒/video=每次视频)"`
|
||||
TaskCallbackUrl string `json:"taskCallbackUrl" dc:"回调地址(仅视频模型)"`
|
||||
}
|
||||
|
||||
type GetUserModelConfigReq struct {
|
||||
g.Meta `path:"/user-model" method:"get" tags:"模型配置" summary:"获取用户模型配置"`
|
||||
}
|
||||
|
||||
type GetUserModelConfigRes struct {
|
||||
*entity.UserModelConfig
|
||||
}
|
||||
|
||||
type SaveUserModelConfigReq struct {
|
||||
g.Meta `path:"/user-model" method:"post" tags:"模型配置" summary:"保存用户模型配置"`
|
||||
ModelConfigId int64 `json:"modelConfigId" dc:"关联模型配置ID"`
|
||||
ApiKey string `json:"apiKey" dc:"用户API密钥"`
|
||||
Temperature float64 `json:"temperature" dc:"温度参数"`
|
||||
MaxTokens int `json:"maxTokens" dc:"最大Token数(不超过系统配置)"`
|
||||
IsActive int `json:"isActive" dc:"是否使用中(0=否 1=是)"`
|
||||
}
|
||||
|
||||
// GetUserModelListReq 获取用户模型配置列表
|
||||
type GetUserModelListReq struct {
|
||||
g.Meta `path:"/user-model-list" method:"get" tags:"模型配置" summary:"获取系统模型列表及用户配置状态"`
|
||||
}
|
||||
|
||||
// UserModelItem 系统模型项及用户配置状态
|
||||
type UserModelItem struct {
|
||||
ModelConfigId int64 `json:"modelConfigId" dc:"系统模型配置ID"`
|
||||
ModelType string `json:"modelType" dc:"chat=对话模型 video=视频模型"`
|
||||
ModelName string `json:"modelName" dc:"模型名称"`
|
||||
Configured bool `json:"configured" dc:"用户是否已配置"`
|
||||
UserApiKey string `json:"userApiKey,omitempty" dc:"用户配置的API Key"`
|
||||
Temperature float64 `json:"temperature,omitempty" dc:"用户配置的温度"`
|
||||
UserMaxTokens int `json:"userMaxTokens,omitempty" dc:"用户配置的max_tokens"`
|
||||
SystemMaxTokens int `json:"systemMaxTokens,omitempty" dc:"系统max_tokens上限"`
|
||||
}
|
||||
|
||||
// GetUserModelListRes 用户模型配置列表响应
|
||||
type GetUserModelListRes struct {
|
||||
List []*UserModelItem `json:"list"`
|
||||
}
|
||||
|
||||
type GetPaymentConfigReq struct {
|
||||
|
||||
@@ -4,20 +4,19 @@ import (
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
// ModelConfig 模型配置(每行一个模型,chat/video 分开存储)
|
||||
type ModelConfig struct {
|
||||
Id int64 `orm:"id" json:"id" dc:"配置ID"`
|
||||
ChatApiKey string `orm:"chat_api_key" json:"chatApiKey" dc:"对话模型API密钥"`
|
||||
ChatBaseUrl string `orm:"chat_base_url" json:"chatBaseUrl" dc:"对话模型接口地址"`
|
||||
ChatModelName string `orm:"chat_model_name" json:"chatModelName" dc:"对话模型名称"`
|
||||
MaxTokens int `orm:"max_tokens" json:"maxTokens" dc:"最大Token数"`
|
||||
Temperature float64 `orm:"temperature" json:"temperature" dc:"温度参数"`
|
||||
ChatSchema string `orm:"chat_schema" json:"chatSchema" dc:"对话模型schema"`
|
||||
VideoApiKey string `orm:"video_api_key" json:"videoApiKey" dc:"视频模型API密钥"`
|
||||
VideoBaseUrl string `orm:"video_base_url" json:"videoBaseUrl" dc:"视频模型接口地址"`
|
||||
VideoModelName string `orm:"video_model_name" json:"videoModelName" dc:"视频模型名称"`
|
||||
VideoSchema string `orm:"video_schema" json:"videoSchema" dc:"视频生成模型schema(JSON, 含duration/ref/prams/sizes)"`
|
||||
PricePerSecond int `orm:"price_per_second" json:"pricePerSecond" dc:"每秒价格(分)"`
|
||||
VideoTaskCallbackUrl string `orm:"video_task_callback_url" json:"videoTaskCallbackUrl" dc:"视频生成任务回调地址"`
|
||||
CreatedAt *gtime.Time `orm:"created_at" json:"createdAt" dc:"创建时间"`
|
||||
UpdatedAt *gtime.Time `orm:"updated_at" json:"updatedAt" dc:"更新时间"`
|
||||
Id int64 `orm:"id" json:"id" dc:"配置ID"`
|
||||
ModelType string `orm:"model_type" json:"modelType" dc:"chat=对话模型 video=视频模型"`
|
||||
ModelName string `orm:"model_name" json:"modelName" dc:"模型名称"`
|
||||
ApiKey string `json:"apiKey" dc:"API密钥"`
|
||||
BaseUrl string `orm:"base_url" json:"baseUrl" dc:"API接口地址"`
|
||||
Schema string `orm:"schema" json:"schema" dc:"请求体JSON Schema"`
|
||||
MaxTokens int `orm:"max_tokens" json:"maxTokens" dc:"最大Token数(仅对话模型)"`
|
||||
Price int `orm:"price" json:"price" dc:"价格(分)"`
|
||||
PriceUnit string `orm:"price_unit" json:"priceUnit" dc:"价格单位(second=每秒/video=每次视频)"`
|
||||
TaskCallbackUrl string `orm:"task_callback_url" json:"taskCallbackUrl" dc:"回调地址(仅视频模型)"`
|
||||
Temperature float64 `json:"temperature" dc:"温度参数(仅运行时合并用户配置用,不持久化)"`
|
||||
CreatedAt *gtime.Time `orm:"created_at" json:"createdAt" dc:"创建时间"`
|
||||
UpdatedAt *gtime.Time `orm:"updated_at" json:"updatedAt" dc:"更新时间"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package entity
|
||||
|
||||
import "github.com/gogf/gf/v2/os/gtime"
|
||||
|
||||
// UserModelConfig 用户级别模型配置
|
||||
// 每行对应一个用户对一个系统模型的 API Key 等私有配置。
|
||||
type UserModelConfig struct {
|
||||
Id int64 `orm:"id" json:"id" dc:"配置ID"`
|
||||
UserId int64 `orm:"user_id" json:"userId" dc:"用户ID"`
|
||||
ModelConfigId int64 `orm:"model_config_id" json:"modelConfigId" dc:"关联模型配置ID"`
|
||||
ApiKey string `orm:"api_key" json:"apiKey" dc:"用户API密钥"`
|
||||
Temperature float64 `orm:"temperature" json:"temperature" dc:"温度参数"`
|
||||
MaxTokens int `orm:"max_tokens" json:"maxTokens" dc:"最大Token数(不超过系统配置)"`
|
||||
IsActive int `orm:"is_active" json:"isActive" dc:"是否使用中(0=否 1=是)"`
|
||||
CreatedAt *gtime.Time `orm:"created_at" json:"createdAt" dc:"创建时间"`
|
||||
UpdatedAt *gtime.Time `orm:"updated_at" json:"updatedAt" dc:"更新时间"`
|
||||
}
|
||||
@@ -356,8 +356,8 @@ func (s *authService) CheckBalance(ctx context.Context, customerId int64, durati
|
||||
if cp == nil {
|
||||
return false, 0, errors.New("客户不存在")
|
||||
}
|
||||
cfg := ConfigService.Get(ctx)
|
||||
cost := durationSec * int64(cfg.PricePerSecond)
|
||||
cfg := ConfigService.GetActiveModel(ctx, "video")
|
||||
cost := durationSec * int64(cfg.Price)
|
||||
return cp.Balance >= cost, cost, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -2,78 +2,221 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"video-factory/shortdrama/dao"
|
||||
"video-factory/shortdrama/model/dto"
|
||||
"video-factory/shortdrama/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gcache"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
const cacheKeyConfig = "model_config"
|
||||
const cacheKeyUserConfigPrefix = "user_model_config_"
|
||||
|
||||
const cacheKeyModelList = "model_config_list"
|
||||
|
||||
// ConfigService 模型配置服务
|
||||
type configService struct{}
|
||||
|
||||
var ConfigService = new(configService)
|
||||
|
||||
// Get 获取模型配置实体(供内部调用,schema 为 string 类型)
|
||||
func (s *configService) Get(ctx context.Context) *entity.ModelConfig {
|
||||
if v, err := gcache.Get(ctx, cacheKeyConfig); err == nil && !v.IsNil() {
|
||||
return v.Val().(*entity.ModelConfig)
|
||||
// schemaMaxTokens 从 schema JSON 中解析 max_tokens 值
|
||||
// 查找路径:properties.parameters.properties.max_tokens.default(JSON Schema 格式)
|
||||
func schemaMaxTokens(schemaStr string) int {
|
||||
if schemaStr == "" {
|
||||
return 4096
|
||||
}
|
||||
|
||||
cfg, err := dao.ModelConfig.GetFirst(ctx)
|
||||
if err == nil && cfg != nil {
|
||||
_ = gcache.Set(ctx, cacheKeyConfig, cfg, 0)
|
||||
return cfg
|
||||
var doc map[string]any
|
||||
if err := json.Unmarshal([]byte(schemaStr), &doc); err != nil {
|
||||
return 4096
|
||||
}
|
||||
|
||||
m := &entity.ModelConfig{}
|
||||
_ = gcache.Set(ctx, cacheKeyConfig, m, 0)
|
||||
return m
|
||||
// 尝试 parameters.properties.max_tokens.default
|
||||
if params, _ := doc["properties"].(map[string]any); params != nil {
|
||||
if p, _ := params["parameters"].(map[string]any); p != nil {
|
||||
if props, _ := p["properties"].(map[string]any); props != nil {
|
||||
if mt, _ := props["max_tokens"].(map[string]any); mt != nil {
|
||||
if def, ok := mt["default"].(float64); ok {
|
||||
return int(def)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 4096
|
||||
}
|
||||
|
||||
// GetResponse 获取模型配置 DTO(给 Controller 返回前端,schema 为 *json.RawMessage)
|
||||
func (s *configService) GetResponse(ctx context.Context) *dto.GetModelConfigRes {
|
||||
cfg := s.Get(ctx)
|
||||
res := &dto.GetModelConfigRes{ModelConfig: cfg}
|
||||
gconv.Struct(cfg.ChatSchema, &res.ChatSchema)
|
||||
gconv.Struct(cfg.VideoSchema, &res.VideoSchema)
|
||||
return res
|
||||
// getModelList 获取模型配置列表(缓存)
|
||||
func (s *configService) getModelList(ctx context.Context) []*entity.ModelConfig {
|
||||
if v, err := gcache.Get(ctx, cacheKeyModelList); err == nil && !v.IsNil() {
|
||||
return v.Val().([]*entity.ModelConfig)
|
||||
}
|
||||
list, err := dao.ModelConfig.GetAll(ctx)
|
||||
if err != nil || len(list) == 0 {
|
||||
return make([]*entity.ModelConfig, 0)
|
||||
}
|
||||
_ = gcache.Set(ctx, cacheKeyModelList, list, 0)
|
||||
return list
|
||||
}
|
||||
|
||||
// Save 保存模型配置(接收 DTO,内部用 gconv.Struct 转换)
|
||||
func (s *configService) Save(ctx context.Context, req *dto.SaveModelConfigReq) error {
|
||||
existing, _ := dao.ModelConfig.GetFirst(ctx)
|
||||
// clearModelListCache 清除模型列表缓存
|
||||
func (s *configService) ClearModelListCache(ctx context.Context) {
|
||||
_, _ = gcache.Remove(ctx, cacheKeyModelList)
|
||||
}
|
||||
|
||||
// GetActiveModel 获取指定类型已启用的系统模型配置
|
||||
func (s *configService) GetActiveModel(ctx context.Context, modelType string) *entity.ModelConfig {
|
||||
list := s.getModelList(ctx)
|
||||
for _, m := range list {
|
||||
if m.ModelType == modelType {
|
||||
return m
|
||||
}
|
||||
}
|
||||
return &entity.ModelConfig{}
|
||||
}
|
||||
|
||||
// GetModelList 获取所有模型配置列表(供管理端)
|
||||
func (s *configService) GetModelList(ctx context.Context) []*entity.ModelConfig {
|
||||
return s.getModelList(ctx)
|
||||
}
|
||||
|
||||
// GetModelListResponse 获取模型配置列表 DTO
|
||||
func (s *configService) GetModelListResponse(ctx context.Context) *dto.GetModelConfigListRes {
|
||||
list := s.getModelList(ctx)
|
||||
return &dto.GetModelConfigListRes{List: list}
|
||||
}
|
||||
|
||||
// SaveModelConfig 保存单个模型配置
|
||||
func (s *configService) SaveModelConfig(ctx context.Context, req *dto.SaveModelConfigReq) error {
|
||||
cfg := new(entity.ModelConfig)
|
||||
if existing != nil {
|
||||
gconv.Struct(existing, cfg)
|
||||
if req.Id > 0 {
|
||||
existing, _ := dao.ModelConfig.GetAll(ctx)
|
||||
for _, m := range existing {
|
||||
if m.Id == req.Id {
|
||||
gconv.Struct(m, cfg)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
// DTO → Entity(同名按字段名映射,schema 除外)
|
||||
gconv.Struct(req, cfg)
|
||||
|
||||
// 特殊处理:*json.RawMessage → string
|
||||
if req.ChatSchema != nil {
|
||||
cfg.ChatSchema = req.ChatSchema.String()
|
||||
// 处理 Schema: *gjson.Json → string
|
||||
if req.Schema != nil {
|
||||
cfg.Schema = req.Schema.String()
|
||||
}
|
||||
if req.VideoSchema != nil {
|
||||
cfg.VideoSchema = req.VideoSchema.String()
|
||||
}
|
||||
|
||||
syncModelDurationFromAPI(ctx, cfg)
|
||||
|
||||
if err := dao.ModelConfig.Save(ctx, cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_ = gcache.Set(ctx, cacheKeyConfig, cfg, 0)
|
||||
s.ClearModelListCache(ctx)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUserConfig 获取指定用户对指定模型的私有配置
|
||||
func (s *configService) GetUserConfig(ctx context.Context, userId int64) *entity.UserModelConfig {
|
||||
if userId <= 0 {
|
||||
return &entity.UserModelConfig{}
|
||||
}
|
||||
cacheKey := cacheKeyUserConfigPrefix + gconv.String(userId)
|
||||
if v, err := gcache.Get(ctx, cacheKey); err == nil && !v.IsNil() {
|
||||
return v.Val().(*entity.UserModelConfig)
|
||||
}
|
||||
|
||||
cfg, err := dao.UserModelConfig.GetActiveByUserId(ctx, userId)
|
||||
if err != nil || cfg == nil {
|
||||
m := &entity.UserModelConfig{UserId: userId}
|
||||
_ = gcache.Set(ctx, cacheKey, m, 0)
|
||||
return m
|
||||
}
|
||||
_ = gcache.Set(ctx, cacheKey, cfg, 0)
|
||||
return cfg
|
||||
}
|
||||
|
||||
// SaveUserConfig 保存用户私有模型配置
|
||||
// 如果已存在 user_id + model_config_id 的记录则更新,否则新增
|
||||
func (s *configService) SaveUserConfig(ctx context.Context, userId int64, cfg *entity.UserModelConfig) error {
|
||||
cfg.UserId = userId
|
||||
|
||||
// 查询是否已存在该用户的同模型配置
|
||||
existing, _ := dao.UserModelConfig.GetByModelConfigId(ctx, userId, cfg.ModelConfigId)
|
||||
if existing != nil {
|
||||
cfg.Id = existing.Id
|
||||
cfg.CreatedAt = existing.CreatedAt
|
||||
}
|
||||
|
||||
if err := dao.UserModelConfig.Save(ctx, cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
_, _ = gcache.Remove(ctx, cacheKeyUserConfigPrefix+gconv.String(userId))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetMergedConfig 获取系统配置合并用户私有配置的结果
|
||||
// modelType: "chat" 或 "video"
|
||||
func (s *configService) GetMergedConfig(ctx context.Context, userId int64, modelType string) *entity.ModelConfig {
|
||||
sysCfg := s.GetActiveModel(ctx, modelType)
|
||||
merged := *sysCfg
|
||||
|
||||
if userId <= 0 {
|
||||
return &merged
|
||||
}
|
||||
userCfg := s.GetUserConfig(ctx, userId)
|
||||
if userCfg == nil || userCfg.Id <= 0 {
|
||||
return &merged
|
||||
}
|
||||
|
||||
// 用户 API Key 覆盖系统 API Key
|
||||
if userCfg.ApiKey != "" {
|
||||
merged.ApiKey = userCfg.ApiKey
|
||||
}
|
||||
// Temperature 从用户配置读取(仅 chat 模型)
|
||||
if userCfg.Temperature > 0 {
|
||||
merged.Temperature = userCfg.Temperature
|
||||
} else if modelType == "chat" {
|
||||
merged.Temperature = 0.85
|
||||
}
|
||||
// UserMaxTokens 仅 chat 模型有效(不超过系统上限,系统上限从 schema 解析)
|
||||
if modelType == "chat" && userCfg.MaxTokens > 0 {
|
||||
merged.MaxTokens = userCfg.MaxTokens
|
||||
sysMax := schemaMaxTokens(sysCfg.Schema)
|
||||
if merged.MaxTokens > sysMax && sysMax > 0 {
|
||||
merged.MaxTokens = sysMax
|
||||
}
|
||||
}
|
||||
return &merged
|
||||
}
|
||||
|
||||
// GetUserModelList 获取系统模型列表及当前用户的配置状态
|
||||
func (s *configService) GetUserModelList(ctx context.Context, userId int64) *dto.GetUserModelListRes {
|
||||
models := s.getModelList(ctx)
|
||||
list := make([]*dto.UserModelItem, 0, len(models))
|
||||
|
||||
// 加载该用户所有模型配置(不分是否激活),逐模型匹配展示
|
||||
userCfgs, _ := dao.UserModelConfig.GetByUserId(ctx, userId)
|
||||
cfgMap := make(map[int64]*entity.UserModelConfig)
|
||||
for _, uc := range userCfgs {
|
||||
cfgMap[uc.ModelConfigId] = uc
|
||||
}
|
||||
|
||||
for _, m := range models {
|
||||
sysMax := schemaMaxTokens(m.Schema)
|
||||
item := &dto.UserModelItem{
|
||||
ModelConfigId: m.Id,
|
||||
ModelType: m.ModelType,
|
||||
ModelName: m.ModelName,
|
||||
SystemMaxTokens: sysMax,
|
||||
}
|
||||
if uc, ok := cfgMap[m.Id]; ok {
|
||||
item.Configured = uc.ApiKey != ""
|
||||
item.UserApiKey = uc.ApiKey
|
||||
item.Temperature = uc.Temperature
|
||||
item.UserMaxTokens = uc.MaxTokens
|
||||
}
|
||||
list = append(list, item)
|
||||
}
|
||||
return &dto.GetUserModelListRes{List: list}
|
||||
}
|
||||
|
||||
// GetPaymentConfigs 获取所有支付配置
|
||||
func (s *configService) GetPaymentConfigs(ctx context.Context) ([]*entity.PaymentConfig, error) {
|
||||
return dao.PaymentConfigDao.GetAll(ctx)
|
||||
@@ -83,15 +226,3 @@ func (s *configService) GetPaymentConfigs(ctx context.Context) ([]*entity.Paymen
|
||||
func (s *configService) SavePaymentConfig(ctx context.Context, cfg *entity.PaymentConfig) error {
|
||||
return dao.PaymentConfigDao.Save(ctx, cfg)
|
||||
}
|
||||
|
||||
// syncModelDurationFromAPI 保存前校验配置完整性,设置默认值
|
||||
func syncModelDurationFromAPI(ctx context.Context, cfg *entity.ModelConfig) {
|
||||
if cfg.Temperature <= 0 {
|
||||
cfg.Temperature = 0.85
|
||||
g.Log().Infof(ctx, "使用默认 Temperature: 0.85")
|
||||
}
|
||||
if cfg.MaxTokens <= 0 {
|
||||
cfg.MaxTokens = 4096
|
||||
g.Log().Infof(ctx, "使用默认 MaxTokens: 4096")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,11 +235,12 @@ func (s *dramaService) GenerateEpisode(ctx context.Context, dramaId, epId int64,
|
||||
return fmt.Errorf("剧集不存在")
|
||||
}
|
||||
|
||||
modelCfg := ConfigService.Get(ctx)
|
||||
if modelCfg.ChatApiKey == "" || modelCfg.ChatModelName == "" {
|
||||
return fmt.Errorf("模型未配置")
|
||||
chatCfg := ConfigService.GetMergedConfig(ctx, d.UserId, "chat")
|
||||
if chatCfg.ApiKey == "" || chatCfg.ModelName == "" {
|
||||
return fmt.Errorf("聊天模型未配置")
|
||||
}
|
||||
if modelCfg.VideoApiKey == "" || modelCfg.VideoBaseUrl == "" || modelCfg.VideoModelName == "" {
|
||||
videoCfg := ConfigService.GetMergedConfig(ctx, d.UserId, "video")
|
||||
if videoCfg.ApiKey == "" || videoCfg.BaseUrl == "" || videoCfg.ModelName == "" {
|
||||
return fmt.Errorf("视频模型未配置")
|
||||
}
|
||||
// 客户身份:余额检查并预扣费
|
||||
@@ -273,7 +274,7 @@ func (s *dramaService) GenerateEpisode(ctx context.Context, dramaId, epId int64,
|
||||
|
||||
genCtx := agent.WithDramaID(context.Background(), d.Id)
|
||||
|
||||
segDurs := calcSegDurs(d.EpisodeDuration, modelCfg)
|
||||
segDurs := calcSegDurs(d.EpisodeDuration, videoCfg)
|
||||
numSegments := len(segDurs)
|
||||
|
||||
g.Log().Infof(ctx, "第%d集 总时长=%ds 拆分为%d段: %v",
|
||||
@@ -376,13 +377,14 @@ func (s *dramaService) GenerateEpisode(ctx context.Context, dramaId, epId int64,
|
||||
|
||||
// generateOneSegment 生成一段内容:Agent → 保存演员/场景 → 保存脚本 → 提交视频
|
||||
func (s *dramaService) generateOneSegment(ctx context.Context, d *entity.Drama, ep *entity.Episode, taskId int64, segIdx, segDur int, feedback string, genCtx *GenerationContext) error {
|
||||
modelCfg := ConfigService.Get(ctx)
|
||||
chatCfg := ConfigService.GetMergedConfig(ctx, d.UserId, "chat")
|
||||
videoCfg := ConfigService.GetMergedConfig(ctx, d.UserId, "video")
|
||||
|
||||
// 使用预加载的场景/道具/演员
|
||||
characters := genCtx.Characters
|
||||
|
||||
// 计算本段在整集中的起始时间(累计前几段时长)
|
||||
segDurs := calcSegDurs(d.EpisodeDuration, modelCfg)
|
||||
segDurs := calcSegDurs(d.EpisodeDuration, videoCfg)
|
||||
segStartTime := 0
|
||||
for j := 0; j < segIdx; j++ {
|
||||
segStartTime += segDurs[j]
|
||||
@@ -395,7 +397,7 @@ func (s *dramaService) generateOneSegment(ctx context.Context, d *entity.Drama,
|
||||
segOutput = buildSegOutputFromShots(ep.Script, segIdx, segStartTime, segDur, genCtx)
|
||||
g.Log().Infof(ctx, "第%d集第%d段 JSON镜头直接提交: %d个镜头", ep.Index, segIdx+1, len(segOutput.Scenes))
|
||||
} else {
|
||||
result, err := s.generateSegment(ctx, d, ep, segIdx, segDur, segStartTime, totalSegs, modelCfg, feedback, genCtx)
|
||||
result, err := s.generateSegment(ctx, d, ep, segIdx, segDur, segStartTime, totalSegs, chatCfg, feedback, genCtx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -462,76 +464,51 @@ func (s *dramaService) generateOneSegment(ctx context.Context, d *entity.Drama,
|
||||
numSegments = task.NumSegments
|
||||
}
|
||||
|
||||
// 提交视频合成
|
||||
// 构建引用列表:从Agent输出的角色名匹配预加载的参考图片
|
||||
// 角色参考:从 segOutput 中选取本段涉及的角色
|
||||
var videoRefs []model.VideoRef
|
||||
|
||||
// 1. 角色参考
|
||||
for _, ch := range segOutput.Characters {
|
||||
mediaURL := genCtx.LookupRef("演员", ch.Name)
|
||||
if mediaURL == "" {
|
||||
continue
|
||||
if url := genCtx.LookupRef("演员", ch.Name); url != "" {
|
||||
videoRefs = append(videoRefs, model.VideoRef{
|
||||
Type: "character", Name: ch.Name, MediaURL: url,
|
||||
})
|
||||
}
|
||||
videoRefs = append(videoRefs, model.VideoRef{
|
||||
Type: "character", Name: ch.Name, MediaURL: mediaURL,
|
||||
})
|
||||
}
|
||||
|
||||
// 2. 场景和道具参考:从脚本镜头中解析本段时间范围内涉及的场景和道具
|
||||
// 场景和道具参考:从脚本镜头中解析本段涉及的场景和道具(按 StartTime 归属,不跨段重复)
|
||||
segEndTime := segStartTime + segDur
|
||||
var coveredScenes []string
|
||||
var coveredProps []string
|
||||
seenScene := make(map[string]bool)
|
||||
seenProp := make(map[string]bool)
|
||||
if domain.IsShotsJSON(ep.Script) {
|
||||
var shots []domain.Shot
|
||||
if err := json.Unmarshal([]byte(ep.Script), &shots); err == nil {
|
||||
for _, sh := range shots {
|
||||
shStart := parseMMSSToSeconds(sh.StartTime)
|
||||
shEnd := parseMMSSToSeconds(sh.EndTime)
|
||||
// 检查镜头是否与本段时间范围重叠
|
||||
if shEnd <= segStartTime || shStart >= segEndTime {
|
||||
if shStart < segStartTime || shStart >= segEndTime {
|
||||
continue
|
||||
}
|
||||
if sh.Scene != "" {
|
||||
coveredScenes = append(coveredScenes, sh.Scene)
|
||||
if sh.Scene != "" && !seenScene[sh.Scene] {
|
||||
seenScene[sh.Scene] = true
|
||||
if url := genCtx.LookupRef("场景", sh.Scene); url != "" {
|
||||
videoRefs = append(videoRefs, model.VideoRef{
|
||||
Type: "scene", Name: sh.Scene, MediaURL: url,
|
||||
})
|
||||
}
|
||||
}
|
||||
for _, p := range sh.Props {
|
||||
if p != "" {
|
||||
coveredProps = append(coveredProps, p)
|
||||
if p != "" && !seenProp[p] {
|
||||
seenProp[p] = true
|
||||
if url := genCtx.LookupRef("道具", p); url != "" {
|
||||
videoRefs = append(videoRefs, model.VideoRef{
|
||||
Type: "prop", Name: p, MediaURL: url,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 去重后添加场景参考
|
||||
seenScene := make(map[string]bool)
|
||||
for _, name := range coveredScenes {
|
||||
if seenScene[name] {
|
||||
continue
|
||||
}
|
||||
seenScene[name] = true
|
||||
if url := genCtx.LookupRef("场景", name); url != "" {
|
||||
videoRefs = append(videoRefs, model.VideoRef{
|
||||
Type: "scene", Name: name, MediaURL: url,
|
||||
})
|
||||
}
|
||||
}
|
||||
// 去重后添加道具参考
|
||||
seenProp := make(map[string]bool)
|
||||
for _, name := range coveredProps {
|
||||
if seenProp[name] {
|
||||
continue
|
||||
}
|
||||
seenProp[name] = true
|
||||
if url := genCtx.LookupRef("道具", name); url != "" {
|
||||
videoRefs = append(videoRefs, model.VideoRef{
|
||||
Type: "prop", Name: name, MediaURL: url,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
g.Log().Infof(ctx, "第%d段视频参考素材: %d个(角色%d 场景%d 道具%d)",
|
||||
segIdx+1, len(videoRefs), len(segOutput.Characters), len(coveredScenes), len(coveredProps))
|
||||
segIdx+1, len(videoRefs), len(segOutput.Characters), len(seenScene), len(seenProp))
|
||||
|
||||
taskID, _, submitErr := s.submitVideoTask(ctx, d, ep, segIdx, segDur, segOutput.Scenes, videoRefs)
|
||||
if submitErr != nil {
|
||||
@@ -567,8 +544,8 @@ func buildSegOutputFromShots(script string, segIdx, segStartTime, segDur int, ge
|
||||
charSet := make(map[string]bool)
|
||||
for _, sh := range allShots {
|
||||
shStart := parseMMSSToSeconds(sh.StartTime)
|
||||
shEnd := parseMMSSToSeconds(sh.EndTime)
|
||||
if shEnd <= segStartTime || shStart >= segEndTime {
|
||||
// 按 StartTime 归属:每个镜头只归入其起始时间所在的段,确保不跨段重复
|
||||
if shStart < segStartTime || shStart >= segEndTime {
|
||||
continue
|
||||
}
|
||||
segShots = append(segShots, sh)
|
||||
@@ -727,9 +704,9 @@ func (s *dramaService) generateSegment(ctx context.Context, d *entity.Drama, ep
|
||||
genCtx *GenerationContext) (string, error) {
|
||||
|
||||
chatCfg := &agent.ModelConfig{
|
||||
ModelName: modelCfg.ChatModelName,
|
||||
APIKey: modelCfg.ChatApiKey,
|
||||
BaseURL: modelCfg.ChatBaseUrl,
|
||||
ModelName: modelCfg.ModelName,
|
||||
APIKey: modelCfg.ApiKey,
|
||||
BaseURL: modelCfg.BaseUrl,
|
||||
MaxTokens: modelCfg.MaxTokens,
|
||||
Temperature: float32(modelCfg.Temperature),
|
||||
}
|
||||
@@ -1017,15 +994,15 @@ func (s *dramaService) FeedbackSegment(ctx context.Context, taskId int64, feedba
|
||||
return err
|
||||
}
|
||||
|
||||
modelCfg := ConfigService.Get(ctx)
|
||||
if modelCfg.VideoApiKey == "" || modelCfg.VideoBaseUrl == "" {
|
||||
return fmt.Errorf("视频模型未配置")
|
||||
}
|
||||
|
||||
d, err := dao.Drama.GetOne(ctx, task.DramaId)
|
||||
if err != nil || d == nil {
|
||||
return fmt.Errorf("短剧不存在")
|
||||
}
|
||||
|
||||
modelCfg := ConfigService.GetMergedConfig(ctx, d.UserId, "video")
|
||||
if modelCfg.ApiKey == "" || modelCfg.BaseUrl == "" {
|
||||
return fmt.Errorf("视频模型未配置")
|
||||
}
|
||||
ep, err := dao.Episode.GetOne(ctx, task.EpisodeId)
|
||||
if err != nil || ep == nil {
|
||||
return fmt.Errorf("剧集不存在")
|
||||
@@ -1048,12 +1025,12 @@ func (s *dramaService) FeedbackSegment(ctx context.Context, taskId int64, feedba
|
||||
}
|
||||
}
|
||||
// 用当前配置的模型名覆盖(兼容数据库配置变更后重做)
|
||||
bodyMap["model"] = modelCfg.VideoModelName
|
||||
bodyMap["model"] = modelCfg.ModelName
|
||||
bodyBytes, _ = json.Marshal(bodyMap)
|
||||
}
|
||||
}
|
||||
|
||||
newTaskID, resolvedBody, submitErr := resubmitVideoTask(genCtx, modelCfg.VideoApiKey, modelCfg.VideoBaseUrl, bodyBytes)
|
||||
newTaskID, resolvedBody, submitErr := resubmitVideoTask(genCtx, modelCfg.ApiKey, modelCfg.BaseUrl, bodyBytes)
|
||||
if submitErr != nil {
|
||||
g.Log().Errorf(genCtx, "第%d集第%d段重新提交视频失败: %v", ep.Index, task.SegmentIdx+1, submitErr)
|
||||
_ = dao.GenerationTask.UpdateFailed(genCtx, taskId, submitErr.Error())
|
||||
@@ -1158,11 +1135,13 @@ func (s *dramaService) StartVideoPoller(ctx context.Context) {
|
||||
|
||||
// pollPendingVideos 扫描所有 generating 任务,轮询或重试视频合成
|
||||
func (s *dramaService) pollPendingVideos(ctx context.Context) {
|
||||
modelCfg := ConfigService.Get(ctx)
|
||||
if modelCfg.VideoApiKey == "" || modelCfg.VideoBaseUrl == "" {
|
||||
modelCfg := ConfigService.GetActiveModel(ctx, "video")
|
||||
if modelCfg.ApiKey == "" || modelCfg.BaseUrl == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// 注意:各任务所属用户不同,轮询时使用 per-task 的 merged config
|
||||
|
||||
allTasks, err := dao.GenerationTask.ListByStatuses(ctx, []string{consts.TaskStatusGenerating, consts.TaskStatusReview})
|
||||
if err != nil || len(allTasks) == 0 {
|
||||
return
|
||||
@@ -1198,7 +1177,7 @@ func (s *dramaService) pollPendingVideos(ctx context.Context) {
|
||||
g.Log().Warningf(ctx, "轮询器: 任务 %d 第%d段视频不支持自定义时长,清除 task_id 准备不携带 duration 重试", task.Id, task.SegmentIdx+1)
|
||||
_ = dao.GenerationTask.UpdateFields(ctx, task.Id, g.Map{"video_task_id": ""})
|
||||
// 持久化探测结果,让后续 calcSegDurs 用保守值切段
|
||||
gcache.Remove(ctx, cacheKeyConfig)
|
||||
ConfigService.ClearModelListCache(ctx)
|
||||
epUpdates[task.EpisodeId] = true
|
||||
} else {
|
||||
g.Log().Warningf(ctx, "轮询器: 任务 %d 第%d段视频任务失败: %v", task.Id, task.SegmentIdx+1, err)
|
||||
@@ -1341,13 +1320,13 @@ func buildPollURL(baseURL, taskId string) string {
|
||||
|
||||
// pollVideoTaskOnce 单次查询视频生成任务状态
|
||||
func (s *dramaService) pollVideoTaskOnce(ctx context.Context, modelCfg *entity.ModelConfig, taskId string) (string, error) {
|
||||
queryURL := buildPollURL(modelCfg.VideoBaseUrl, taskId)
|
||||
queryURL := buildPollURL(modelCfg.BaseUrl, taskId)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", queryURL, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("创建查询请求失败: %w", err)
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+modelCfg.VideoApiKey)
|
||||
req.Header.Set("Authorization", "Bearer "+modelCfg.ApiKey)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
@@ -1430,8 +1409,8 @@ func mapVideoTaskStatus(s string) videoTaskStatus {
|
||||
// submitVideoTask 提交视频合成任务,返回 (taskID, requestBodyJSON, error)
|
||||
// requestBodyJSON 是调用视频模型 API 时发送的完整请求体 JSON 字符串
|
||||
func (s *dramaService) submitVideoTask(ctx context.Context, d *entity.Drama, ep *entity.Episode, segIdx, segDur int, scenes []model.SegmentScene, refs []model.VideoRef) (string, string, error) {
|
||||
modelCfg := ConfigService.Get(ctx)
|
||||
if modelCfg.VideoApiKey == "" || modelCfg.VideoBaseUrl == "" || modelCfg.VideoModelName == "" {
|
||||
modelCfg := ConfigService.GetMergedConfig(ctx, d.UserId, "video")
|
||||
if modelCfg.ApiKey == "" || modelCfg.BaseUrl == "" || modelCfg.ModelName == "" {
|
||||
return "", "", fmt.Errorf("视频模型未配置")
|
||||
}
|
||||
|
||||
@@ -1448,9 +1427,9 @@ func (s *dramaService) submitVideoTask(ctx context.Context, d *entity.Drama, ep
|
||||
// 从 video_schema 读取模型特定配置(结构按 API 请求格式: input / parameters)
|
||||
refMaxCount := 5 // 默认最多5个
|
||||
promptMaxChars := 0
|
||||
if modelCfg.VideoSchema != "" {
|
||||
if modelCfg.Schema != "" {
|
||||
var vs map[string]any
|
||||
if err := json.Unmarshal([]byte(modelCfg.VideoSchema), &vs); err == nil {
|
||||
if err := json.Unmarshal([]byte(modelCfg.Schema), &vs); err == nil {
|
||||
refMaxCount = intVal(nested(vs, "input", "reference_urls", "total_max"), refMaxCount)
|
||||
promptMaxChars = intVal(nested(vs, "input", "prompt", "max_chars"), 0)
|
||||
}
|
||||
@@ -1526,7 +1505,7 @@ func (s *dramaService) submitVideoTask(ctx context.Context, d *entity.Drama, ep
|
||||
}
|
||||
promptRunes := []rune(prompt)
|
||||
if keepLen < len(promptRunes) {
|
||||
prompt = "..." + string(promptRunes[len(promptRunes)-keepLen+3:])
|
||||
prompt = string(promptRunes[:keepLen-3]) + "..."
|
||||
}
|
||||
g.Log().Infof(ctx, "prompt超长已截断至%d字符(原%d字符)", promptMaxChars, len([]rune(prompt)))
|
||||
}
|
||||
@@ -1541,16 +1520,16 @@ func (s *dramaService) submitVideoTask(ctx context.Context, d *entity.Drama, ep
|
||||
|
||||
// 从 video_schema 判断当前模型是否支持 duration 参数
|
||||
effectiveDur := segDur
|
||||
if modelCfg.VideoSchema != "" {
|
||||
if modelCfg.Schema != "" {
|
||||
var vs map[string]any
|
||||
if err := json.Unmarshal([]byte(modelCfg.VideoSchema), &vs); err == nil {
|
||||
if err := json.Unmarshal([]byte(modelCfg.Schema), &vs); err == nil {
|
||||
if durDef := nested(vs, "parameters", "duration"); durDef != nil {
|
||||
if durMap, ok := durDef.(map[string]any); ok {
|
||||
if sm, ok := durMap["supported_models"]; ok {
|
||||
if models, ok := sm.([]any); ok {
|
||||
found := false
|
||||
for _, m := range models {
|
||||
if ms, ok := m.(string); ok && ms == modelCfg.VideoModelName {
|
||||
if ms, ok := m.(string); ok && ms == modelCfg.ModelName {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
@@ -1569,9 +1548,9 @@ func (s *dramaService) submitVideoTask(ctx context.Context, d *entity.Drama, ep
|
||||
}
|
||||
|
||||
// 固定 seed 确保各段画风/人物形象一致(基于短剧ID+剧集序号)
|
||||
seed := int(d.Id)*1000 + ep.Index*10 + segIdx
|
||||
taskId, requestJSON, err := createVideoTask(ctx, modelCfg.VideoApiKey, modelCfg.VideoBaseUrl, modelCfg.VideoModelName,
|
||||
prompt, negativePrompt, refURLs, effectiveDur, d.Resolution, d.AspectRatio, modelCfg.VideoSchema, seed)
|
||||
seed := int(d.Id)*1000 + ep.Index*10
|
||||
taskId, requestJSON, err := createVideoTask(ctx, modelCfg.ApiKey, modelCfg.BaseUrl, modelCfg.ModelName,
|
||||
prompt, negativePrompt, refURLs, effectiveDur, d.Resolution, d.AspectRatio, modelCfg.Schema, seed)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("视频合成请求失败: %w", err)
|
||||
}
|
||||
@@ -1975,8 +1954,8 @@ func (s *dramaService) overlayBackgroundMusic(ctx context.Context, dramaId int64
|
||||
// 轮询视频 API 直到视频就绪,下载到本地,更新 DB 的 video_url。
|
||||
// 用于串行模式中让下一段能提取上一段的尾帧作为首帧。
|
||||
func (s *dramaService) waitForSegmentVideo(ctx context.Context, d *entity.Drama, ep *entity.Episode, taskId int64) error {
|
||||
modelCfg := ConfigService.Get(ctx)
|
||||
if modelCfg.VideoApiKey == "" || modelCfg.VideoBaseUrl == "" {
|
||||
modelCfg := ConfigService.GetMergedConfig(ctx, d.UserId, "video")
|
||||
if modelCfg.ApiKey == "" || modelCfg.BaseUrl == "" {
|
||||
return fmt.Errorf("视频模型未配置或查询地址为空")
|
||||
}
|
||||
|
||||
@@ -2141,9 +2120,9 @@ func calcSegDurs(episodeDuration int64, cfg *entity.ModelConfig) []int {
|
||||
// 从 video_schema.duration 读取模型单段时长约束
|
||||
effectiveMax := 15 // 默认值
|
||||
minSingle := 5
|
||||
if cfg.VideoSchema != "" {
|
||||
if cfg.Schema != "" {
|
||||
var vs map[string]any
|
||||
if err := json.Unmarshal([]byte(cfg.VideoSchema), &vs); err == nil {
|
||||
if err := json.Unmarshal([]byte(cfg.Schema), &vs); err == nil {
|
||||
if v := intVal(nested(vs, "parameters", "duration", "max"), 0); v > 0 {
|
||||
effectiveMax = v
|
||||
}
|
||||
|
||||
@@ -205,8 +205,8 @@ func (s *dramaService) GenerateScript(ctx context.Context, dramaId int64, episod
|
||||
return
|
||||
}
|
||||
|
||||
modelCfg := ConfigService.Get(ctx)
|
||||
if modelCfg.ChatApiKey == "" || modelCfg.ChatModelName == "" {
|
||||
modelCfg := ConfigService.GetMergedConfig(ctx, d.UserId, "chat")
|
||||
if modelCfg.ApiKey == "" || modelCfg.ModelName == "" {
|
||||
err = fmt.Errorf("模型未配置")
|
||||
return
|
||||
}
|
||||
@@ -222,9 +222,9 @@ func (s *dramaService) GenerateScript(ctx context.Context, dramaId int64, episod
|
||||
userInput := s.buildScriptGenUserInput(d, episodeTitle, description, genCtx)
|
||||
|
||||
chatCfg := &agent.ModelConfig{
|
||||
ModelName: modelCfg.ChatModelName,
|
||||
APIKey: modelCfg.ChatApiKey,
|
||||
BaseURL: modelCfg.ChatBaseUrl,
|
||||
ModelName: modelCfg.ModelName,
|
||||
APIKey: modelCfg.ApiKey,
|
||||
BaseURL: modelCfg.BaseUrl,
|
||||
MaxTokens: modelCfg.MaxTokens,
|
||||
Temperature: float32(modelCfg.Temperature),
|
||||
Timeout: time.Duration(g.Cfg().MustGet(ctx, "chat.timeout", 180).Int()) * time.Second,
|
||||
@@ -349,7 +349,7 @@ func createPendingTasks(ctx context.Context, dramaId, epId int64, script string,
|
||||
if err != nil || d == nil {
|
||||
return fmt.Errorf("短剧不存在: %d", dramaId)
|
||||
}
|
||||
modelCfg := ConfigService.Get(ctx)
|
||||
modelCfg := ConfigService.GetActiveModel(ctx, "video")
|
||||
|
||||
// 预加载参考素材(演员优先,场景其次,道具最后)
|
||||
chars, _, _ := dao.Character.ListPageByDrama(ctx, dramaId, 1, -1)
|
||||
@@ -365,8 +365,8 @@ func createPendingTasks(ctx context.Context, dramaId, epId int64, script string,
|
||||
// 从 video_schema 读取参考素材上限和默认参数
|
||||
refMax := 5
|
||||
var vs map[string]any
|
||||
if modelCfg.VideoSchema != "" {
|
||||
json.Unmarshal([]byte(modelCfg.VideoSchema), &vs)
|
||||
if modelCfg.Schema != "" {
|
||||
json.Unmarshal([]byte(modelCfg.Schema), &vs)
|
||||
}
|
||||
if vs != nil {
|
||||
if v := nested(vs, "input", "reference_urls", "total_max"); v != nil {
|
||||
@@ -446,7 +446,7 @@ func createPendingTasks(ctx context.Context, dramaId, epId int64, script string,
|
||||
if models, ok := sm.([]any); ok {
|
||||
found := false
|
||||
for _, m := range models {
|
||||
if ms, ok := m.(string); ok && ms == modelCfg.VideoModelName {
|
||||
if ms, ok := m.(string); ok && ms == modelCfg.ModelName {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
@@ -601,7 +601,7 @@ func createPendingTasks(ctx context.Context, dramaId, epId int64, script string,
|
||||
segParams["duration"] = tg.dur
|
||||
|
||||
bodyJSON, _ := json.Marshal(map[string]any{
|
||||
"model": modelCfg.VideoModelName,
|
||||
"model": modelCfg.ModelName,
|
||||
"input": map[string]any{
|
||||
"prompt": tg.promptText,
|
||||
"negative_prompt": negativePrompt,
|
||||
|
||||
@@ -1 +1,97 @@
|
||||
{"model": "wan2.6-r2v", "input": {"prompt": {"required": true, "type": "string", "max_chars": 1500, "description": "文本提示词。支持中英文,每个汉字/字母/标点占一个字符,超过部分自动截断。通过 character1、character2 引用参考角色,每个参考仅包含单一角色。模型仅通过此方式识别参考中的角色"}, "negative_prompt": {"required": false, "type": "string", "max_chars": 500, "description": "反向提示词,用来描述不希望在视频画面中出现的内容,可以对视频画面进行限制"}, "reference_urls": {"required": true, "type": "array", "items": "string", "ref_ordering": "按数组顺序定义角色顺序,第1个URL对应character1,第2个对应character2,依此类推。每个参考文件仅包含一个主体角色", "total_max": 5, "image_max": 5, "video_max": 3, "video_formats": ["MP4", "MOV"], "video_duration_sec": {"min": 1, "max": 30}, "video_max_size_mb": 100, "image_formats": ["JPEG", "JPG", "PNG", "BMP", "WEBP"], "image_note": "PNG不支持透明通道", "image_resolution_px": {"min": 240, "max": 8000}, "image_max_size_mb": 20}}, "parameters": {"size": {"required": false, "type": "string", "format": "{width}*{height}", "default": "1920*1080", "description": "视频分辨率,格式为宽*高。必须设置为具体数值(如1280*720),而不是1:1或720P", "sizes": {"720P": {"9:16": "720*1280", "16:9": "1280*720", "1:1": "960*960", "4:3": "1088*832", "3:4": "832*1088"}, "1080P": {"9:16": "1080*1920", "16:9": "1920*1080", "1:1": "1440*1440", "4:3": "1632*1248", "3:4": "1248*1632"}}}, "duration": {"required": false, "type": "integer", "min": 2, "max": 10, "default": 5, "description": "生成视频的时长,单位为秒"}, "shot_type": {"required": false, "type": "string", "enum": ["single", "multi"], "default": "single", "description": "镜头类型。参数优先级:shot_type > prompt。single=单镜头视频,multi=多镜头视频"}, "audio": {"required": false, "type": "boolean", "default": true, "supported_models": ["wan2.6-r2v-flash"], "description": "是否生成有声视频"}, "watermark": {"required": false, "type": "boolean", "default": false}, "seed": {"required": false, "type": "integer", "min": 0, "max": 2147483647}}}
|
||||
{
|
||||
"input": {
|
||||
"negative_prompt": {
|
||||
"description": "反向提示词,用来描述不希望在视频画面中出现的内容,可以对视频画面进行限制",
|
||||
"max_chars": 500,
|
||||
"required": false,
|
||||
"type": "string"
|
||||
},
|
||||
"prompt": {
|
||||
"description": "文本提示词。支持中英文,每个汉字/字母/标点占一个字符,超过部分自动截断。通过 character1、character2 引用参考角色,每个参考仅包含单一角色。模型仅通过此方式识别参考中的角色",
|
||||
"max_chars": 1500,
|
||||
"required": true,
|
||||
"type": "string"
|
||||
},
|
||||
"reference_urls": {
|
||||
"image_formats": ["JPEG", "JPG", "PNG", "BMP", "WEBP"],
|
||||
"image_max": 5,
|
||||
"image_max_size_mb": 20,
|
||||
"image_note": "PNG不支持透明通道",
|
||||
"image_resolution_px": {
|
||||
"max": 8000,
|
||||
"min": 240
|
||||
},
|
||||
"items": "string",
|
||||
"ref_ordering": "按数组顺序定义角色顺序,第1个URL对应character1,第2个对应character2,依此类推。每个参考文件仅包含一个主体角色",
|
||||
"required": true,
|
||||
"total_max": 5,
|
||||
"type": "array",
|
||||
"video_duration_sec": {
|
||||
"max": 30,
|
||||
"min": 1
|
||||
},
|
||||
"video_formats": ["MP4", "MOV"],
|
||||
"video_max": 3,
|
||||
"video_max_size_mb": 100
|
||||
}
|
||||
},
|
||||
"model": "wan2.6-r2v-flash",
|
||||
"parameters": {
|
||||
"audio": {
|
||||
"default": true,
|
||||
"description": "是否生成有声视频",
|
||||
"required": false,
|
||||
"supported_models": ["wan2.6-r2v-flash"],
|
||||
"type": "boolean"
|
||||
},
|
||||
"duration": {
|
||||
"default": 5,
|
||||
"description": "生成视频的时长,单位为秒",
|
||||
"max": 10,
|
||||
"min": 2,
|
||||
"required": false,
|
||||
"type": "integer"
|
||||
},
|
||||
"seed": {
|
||||
"max": 2147483647,
|
||||
"min": 0,
|
||||
"required": false,
|
||||
"type": "integer"
|
||||
},
|
||||
"shot_type": {
|
||||
"default": "single",
|
||||
"description": "镜头类型。参数优先级:shot_type \u003e prompt。single=单镜头视频,multi=多镜头视频",
|
||||
"enum": ["single", "multi"],
|
||||
"required": false,
|
||||
"type": "string"
|
||||
},
|
||||
"size": {
|
||||
"default": "1920*1080",
|
||||
"description": "视频分辨率,格式为宽*高。必须设置为具体数值(如1280*720),而不是1:1或720P",
|
||||
"format": "{width}*{height}",
|
||||
"required": false,
|
||||
"sizes": {
|
||||
"1080P": {
|
||||
"16:9": "1920*1080",
|
||||
"1:1": "1440*1440",
|
||||
"3:4": "1248*1632",
|
||||
"4:3": "1632*1248",
|
||||
"9:16": "1080*1920"
|
||||
},
|
||||
"720P": {
|
||||
"16:9": "1280*720",
|
||||
"1:1": "960*960",
|
||||
"3:4": "832*1088",
|
||||
"4:3": "1088*832",
|
||||
"9:16": "720*1280"
|
||||
}
|
||||
},
|
||||
"type": "string"
|
||||
},
|
||||
"watermark": {
|
||||
"default": false,
|
||||
"required": false,
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user