1
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
const jwtSecret = "video-factory-jwt-secret-2024"
|
||||
|
||||
type JwtClaims struct {
|
||||
UserId int64 `json:"user_id"`
|
||||
Role string `json:"role"`
|
||||
AgentId int64 `json:"agent_id,omitempty"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
func GetJwtSecret() string {
|
||||
return jwtSecret
|
||||
}
|
||||
|
||||
func ParseToken(tokenStr string) (*JwtClaims, error) {
|
||||
token, err := jwt.ParseWithClaims(tokenStr, &JwtClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(jwtSecret), nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
claims, ok := token.Claims.(*JwtClaims)
|
||||
if !ok || !token.Valid {
|
||||
return nil, errors.New("invalid token")
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
@@ -1,47 +1,16 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
const jwtSecret = "video-factory-jwt-secret-2024"
|
||||
|
||||
func GetJwtSecret() string {
|
||||
return jwtSecret
|
||||
var publicPaths = []string{
|
||||
"/user/login",
|
||||
}
|
||||
|
||||
type JwtClaims struct {
|
||||
UserId int64 `json:"user_id"`
|
||||
Role string `json:"role"`
|
||||
AgentId int64 `json:"agent_id,omitempty"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
func ParseToken(tokenStr string) (*JwtClaims, error) {
|
||||
token, err := jwt.ParseWithClaims(tokenStr, &JwtClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(jwtSecret), nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
claims, ok := token.Claims.(*JwtClaims)
|
||||
if !ok || !token.Valid {
|
||||
return nil, errors.New("invalid token")
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
var (
|
||||
publicPaths = []string{
|
||||
"/user/login",
|
||||
}
|
||||
)
|
||||
|
||||
func Auth(r *ghttp.Request) {
|
||||
path := r.URL.Path
|
||||
for _, p := range publicPaths {
|
||||
|
||||
+208
@@ -2,6 +2,8 @@ package common
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
@@ -34,3 +36,209 @@ func pathExt(path string) string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// BuildSchemaRequest validates input values against a JSON schema definition,
|
||||
// fills in default values for missing optional fields,
|
||||
// and returns the result matching the schema's nested structure.
|
||||
//
|
||||
// The schema format follows test.json convention:
|
||||
//
|
||||
// {
|
||||
// "section": {
|
||||
// "field_name": {
|
||||
// "type": "string|integer|number|boolean|array|object",
|
||||
// "required": true|false,
|
||||
// "default": value,
|
||||
// "enum": [...],
|
||||
// "min": number,
|
||||
// "max": number,
|
||||
// "max_chars": number,
|
||||
// "min_items": number,
|
||||
// "max_items": number
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// input is a flat map like {"prompt": "hello", "duration": 5}.
|
||||
// Fields not present in input but with a "default" in the schema are filled automatically.
|
||||
// Nodes without "type" are treated as grouping sections and recursed into.
|
||||
func BuildSchemaRequest(schema map[string]any, input map[string]any) (map[string]any, error) {
|
||||
result := make(map[string]any)
|
||||
for key, val := range schema {
|
||||
fieldDef, ok := val.(map[string]any)
|
||||
if !ok {
|
||||
result[key] = val
|
||||
continue
|
||||
}
|
||||
if _, hasType := fieldDef["type"]; hasType {
|
||||
processed, err := processField(key, fieldDef, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if processed != nil {
|
||||
result[key] = processed
|
||||
}
|
||||
continue
|
||||
}
|
||||
nested, err := BuildSchemaRequest(fieldDef, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(nested) > 0 {
|
||||
result[key] = nested
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func processField(name string, def map[string]any, input map[string]any) (any, error) {
|
||||
fieldType, _ := def["type"].(string)
|
||||
required, _ := def["required"].(bool)
|
||||
|
||||
rawVal, exists := input[name]
|
||||
if !exists {
|
||||
if required {
|
||||
return nil, fmt.Errorf("'%s' is required", name)
|
||||
}
|
||||
if dflt, ok := def["default"]; ok {
|
||||
return convertDefault(dflt, fieldType), nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
switch fieldType {
|
||||
case "string":
|
||||
s, ok := rawVal.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("'%s' must be a string", name)
|
||||
}
|
||||
if maxChars, ok := def["max_chars"].(float64); ok && len(s) > int(maxChars) {
|
||||
return nil, fmt.Errorf("'%s' exceeds max length of %d", name, int(maxChars))
|
||||
}
|
||||
if enum, ok := def["enum"].([]any); ok && len(enum) > 0 {
|
||||
if !containsValue(enum, s) {
|
||||
return nil, fmt.Errorf("'%s' must be one of %v", name, enum)
|
||||
}
|
||||
}
|
||||
return s, nil
|
||||
|
||||
case "integer":
|
||||
v, err := toInt(rawVal)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("'%s' must be an integer", name)
|
||||
}
|
||||
if minVal, ok := def["min"].(float64); ok && v < int(minVal) {
|
||||
return nil, fmt.Errorf("'%s' must be >= %d", name, int(minVal))
|
||||
}
|
||||
if maxVal, ok := def["max"].(float64); ok && v > int(maxVal) {
|
||||
return nil, fmt.Errorf("'%s' must be <= %d", name, int(maxVal))
|
||||
}
|
||||
return v, nil
|
||||
|
||||
case "number":
|
||||
v, ok := rawVal.(float64)
|
||||
if !ok {
|
||||
if iv, err := toInt(rawVal); err == nil {
|
||||
v = float64(iv)
|
||||
} else {
|
||||
return nil, fmt.Errorf("'%s' must be a number", name)
|
||||
}
|
||||
}
|
||||
if minVal, ok := def["min"].(float64); ok && v < minVal {
|
||||
return nil, fmt.Errorf("'%s' must be >= %v", name, minVal)
|
||||
}
|
||||
if maxVal, ok := def["max"].(float64); ok && v > maxVal {
|
||||
return nil, fmt.Errorf("'%s' must be <= %v", name, maxVal)
|
||||
}
|
||||
return v, nil
|
||||
|
||||
case "boolean":
|
||||
_, ok := rawVal.(bool)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("'%s' must be a boolean", name)
|
||||
}
|
||||
return rawVal, nil
|
||||
|
||||
case "array":
|
||||
arr, ok := rawVal.([]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("'%s' must be an array", name)
|
||||
}
|
||||
if minItems, ok := def["min_items"].(float64); ok && len(arr) < int(minItems) {
|
||||
return nil, fmt.Errorf("'%s' must have at least %d items", name, int(minItems))
|
||||
}
|
||||
if maxItems, ok := def["max_items"].(float64); ok && len(arr) > int(maxItems) {
|
||||
return nil, fmt.Errorf("'%s' must have at most %d items", name, int(maxItems))
|
||||
}
|
||||
if itemsDef, ok := def["items"].(map[string]any); ok {
|
||||
items, err := processArrayItems(arr, itemsDef)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("'%s': %w", name, err)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
return arr, nil
|
||||
}
|
||||
|
||||
return rawVal, nil
|
||||
}
|
||||
|
||||
func processArrayItems(arr []any, itemsDef map[string]any) ([]any, error) {
|
||||
itemType, _ := itemsDef["type"].(string)
|
||||
if itemType != "object" {
|
||||
return arr, nil
|
||||
}
|
||||
props, _ := itemsDef["properties"].(map[string]any)
|
||||
if props == nil {
|
||||
return arr, nil
|
||||
}
|
||||
|
||||
result := make([]any, len(arr))
|
||||
for i, item := range arr {
|
||||
itemMap, ok := item.(map[string]any)
|
||||
if !ok {
|
||||
result[i] = item
|
||||
continue
|
||||
}
|
||||
processed, err := BuildSchemaRequest(props, itemMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("item[%d]: %w", i, err)
|
||||
}
|
||||
result[i] = processed
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func toInt(v any) (int, error) {
|
||||
switch val := v.(type) {
|
||||
case float64:
|
||||
return int(val), nil
|
||||
case int:
|
||||
return val, nil
|
||||
case int64:
|
||||
return int(val), nil
|
||||
case json.Number:
|
||||
n, err := val.Int64()
|
||||
return int(n), err
|
||||
default:
|
||||
return 0, fmt.Errorf("cannot convert %T to int", v)
|
||||
}
|
||||
}
|
||||
|
||||
func convertDefault(dflt any, fieldType string) any {
|
||||
if fieldType == "integer" {
|
||||
if f, ok := dflt.(float64); ok {
|
||||
return int(f)
|
||||
}
|
||||
}
|
||||
return dflt
|
||||
}
|
||||
|
||||
func containsValue(arr []any, val any) bool {
|
||||
for _, v := range arr {
|
||||
if v == val {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"video-factory/common"
|
||||
"video-factory/shortdrama/agent"
|
||||
consts "video-factory/shortdrama/consts"
|
||||
"video-factory/shortdrama/dao"
|
||||
@@ -403,59 +404,6 @@ func createPendingTasks(ctx context.Context, dramaId, epId int64, script string,
|
||||
minSingle = effectiveMax
|
||||
}
|
||||
|
||||
// 从 video_schema 构建完整请求体结构(所有默认值)
|
||||
bodyBase := map[string]any{}
|
||||
if vs != nil {
|
||||
if bodyDef, ok := nested(vs, "body").(map[string]any); ok {
|
||||
for sectionKey, sectionVal := range bodyDef {
|
||||
sectionFields, ok := sectionVal.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
sectionOut := map[string]any{}
|
||||
for fieldKey, fieldDef := range sectionFields {
|
||||
fd, ok := fieldDef.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
// parameters 支持 supported_models 过滤
|
||||
if sectionKey == "parameters" {
|
||||
if sm, ok := fd["supported_models"]; ok {
|
||||
if models, ok := sm.([]any); ok {
|
||||
found := false
|
||||
for _, m := range models {
|
||||
if ms, ok := m.(string); ok && ms == modelCfg.ModelName {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 始终包含 schema 中定义的所有字段(无 default 的字段留 nil,后续覆盖)
|
||||
if def, ok := fd["default"]; ok {
|
||||
sectionOut[fieldKey] = def
|
||||
} else {
|
||||
sectionOut[fieldKey] = nil
|
||||
}
|
||||
}
|
||||
bodyBase[sectionKey] = sectionOut
|
||||
}
|
||||
}
|
||||
// 尺寸映射:从 drama 的 Resolution/AspectRatio 查 video_schema
|
||||
if sizes := nested(vs, "body", "parameters", "size", "sizes"); sizes != nil {
|
||||
if sm, ok := sizes.(map[string]any); ok {
|
||||
if v := resolveSizeFromSchema(sm, d.Resolution, d.AspectRatio); v != "" {
|
||||
if params, ok := bodyBase["parameters"].(map[string]any); ok {
|
||||
params["size"] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// ============ 加载参考素材 ============
|
||||
type _namedRef struct {
|
||||
path string
|
||||
@@ -596,38 +544,39 @@ func createPendingTasks(ctx context.Context, dramaId, epId int64, script string,
|
||||
}
|
||||
|
||||
// ============ 插入任务 ============
|
||||
bodyDef, _ := nested(vs, "body").(map[string]any)
|
||||
for i, tg := range taskGroups {
|
||||
// 深拷贝 bodyBase,覆盖动态字段
|
||||
body := make(map[string]any, len(bodyBase))
|
||||
for bk, bv := range bodyBase {
|
||||
if section, ok := bv.(map[string]any); ok {
|
||||
nested := make(map[string]any, len(section))
|
||||
for sk, sv := range section {
|
||||
nested[sk] = sv
|
||||
}
|
||||
body[bk] = nested
|
||||
} else {
|
||||
body[bk] = bv
|
||||
}
|
||||
segInput := map[string]any{
|
||||
"prompt": buildFinalPrompt(tg.promptText),
|
||||
"duration": tg.dur,
|
||||
"seed": seed,
|
||||
}
|
||||
body["model"] = modelCfg.ModelName
|
||||
setSchemaField(body, "prompt", buildFinalPrompt(tg.promptText))
|
||||
if negativePrompt != "" {
|
||||
setSchemaField(body, "negative_prompt", negativePrompt)
|
||||
segInput["negative_prompt"] = negativePrompt
|
||||
}
|
||||
setSchemaField(body, "duration", tg.dur)
|
||||
// 参考素材
|
||||
if len(refURLs) > 0 {
|
||||
_, mediaTypeField, mediaURLField, _ := resolveMediaFields(modelCfg.Schema, modelCfg.FirstFrameMapping)
|
||||
setSchemaField(body, "media", buildSchemaMedia(refURLs, mediaTypeField, mediaURLField))
|
||||
setSchemaField(body, "reference_urls", refURLs)
|
||||
} else if getSchemaField(body, "media") == nil {
|
||||
setSchemaField(body, "media", []any{})
|
||||
segInput["media"] = buildSchemaMedia(refURLs, mediaTypeField, mediaURLField)
|
||||
refsAny := make([]any, len(refURLs))
|
||||
for j, u := range refURLs {
|
||||
refsAny[j] = u
|
||||
}
|
||||
segInput["reference_urls"] = refsAny
|
||||
}
|
||||
if seed > 0 {
|
||||
setSchemaField(body, "seed", seed)
|
||||
body, _ := common.BuildSchemaRequest(bodyDef, segInput)
|
||||
if body == nil {
|
||||
body = map[string]any{}
|
||||
}
|
||||
body["model"] = modelCfg.ModelName
|
||||
if sizes := nested(vs, "body", "parameters", "size", "sizes"); sizes != nil {
|
||||
if sm, ok := sizes.(map[string]any); ok {
|
||||
if v := resolveSizeFromSchema(sm, d.Resolution, d.AspectRatio); v != "" {
|
||||
if params, ok := body["parameters"].(map[string]any); ok {
|
||||
params["size"] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 非第0段添加首帧占位(URL 为空),第0段不需要首帧参考
|
||||
if i > 0 {
|
||||
injectFirstFrame(body, "", modelCfg.FirstFrameMapping, modelCfg.Schema)
|
||||
}
|
||||
|
||||
@@ -783,7 +783,7 @@ func (s *generationService) ListEpisodeTasks(ctx context.Context, epId int64) ([
|
||||
return dao.GenerationTask.ListByEpisode(ctx, epId)
|
||||
}
|
||||
|
||||
// createVideoTask 调用视频生成API提交任务,返回 (taskID, requestBodyJSON, error)
|
||||
// submitVideoGenRequest 构建请求体并调用视频生成API,返回 (taskID, requestBodyJSON, error)
|
||||
|
||||
func (s *generationService) mergeEpisodeVideo(ctx context.Context, epId int64, lastTaskId int64) error {
|
||||
tasks, err := dao.GenerationTask.ListByEpisode(ctx, epId)
|
||||
@@ -2118,7 +2118,7 @@ func (s *generationService) submitVideoTask(ctx context.Context, d *entity.Drama
|
||||
|
||||
// 固定 seed 确保各段画风/人物形象一致(基于短剧ID+剧集序号)
|
||||
seed := int(d.Id)*1000 + ep.Index*10
|
||||
taskId, requestJSON, err := createVideoTask(ctx, modelCfg.ApiKey, modelCfg.BaseUrl, modelCfg.ModelName,
|
||||
taskId, requestJSON, err := submitVideoGenRequest(ctx, modelCfg.ApiKey, modelCfg.BaseUrl, modelCfg.ModelName,
|
||||
prompt, negativePrompt, refURLs, effectiveDur, d.Resolution, d.AspectRatio, modelCfg.Schema, seed, segIdx, genTaskId, ep.Id, d.Title, ep.Title, modelCfg.FirstFrameMapping)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("video composition request failed: %w", err)
|
||||
@@ -2178,78 +2178,53 @@ func callVideoAPI(ctx context.Context, apiKey, baseURL string, payload []byte, s
|
||||
//
|
||||
// params: 额外参数(如 audio/shot_type/watermark 等)
|
||||
// sizes: 分辨率→尺寸字符串映射表
|
||||
func createVideoTask(ctx context.Context, apiKey, baseURL, modelName, prompt, negativePrompt string, refURLs []string, duration int, resolution, aspectRatio, videoSchema string, seed int, segIdx int, genTaskId int64, episodeId int64, dramaTitle, epTitle, firstFrameMapping string) (string, string, error) {
|
||||
// 从 schema body 构建请求体基础结构(vs 只解析一次)
|
||||
body := map[string]any{}
|
||||
func submitVideoGenRequest(ctx context.Context, apiKey, baseURL, modelName, prompt, negativePrompt string, refURLs []string, duration int, resolution, aspectRatio, videoSchema string, seed int, segIdx int, genTaskId int64, episodeId int64, dramaTitle, epTitle, firstFrameMapping string) (string, string, error) {
|
||||
var vs map[string]any
|
||||
if videoSchema != "" {
|
||||
json.Unmarshal([]byte(videoSchema), &vs)
|
||||
}
|
||||
if bodyDef, ok := nested(vs, "body").(map[string]any); ok {
|
||||
for sectionKey, sectionVal := range bodyDef {
|
||||
sectionFields, ok := sectionVal.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
sectionOut := map[string]any{}
|
||||
for fieldKey, fieldDef := range sectionFields {
|
||||
fd, ok := fieldDef.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
// parameters 支持 supported_models 过滤
|
||||
if sectionKey == "parameters" {
|
||||
if sm, ok := fd["supported_models"]; ok {
|
||||
if models, ok := sm.([]any); ok {
|
||||
found := false
|
||||
for _, m := range models {
|
||||
if ms, ok := m.(string); ok && ms == modelName {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 始终包含 schema 中定义的所有字段(无 default 的字段留 nil,后续覆盖)
|
||||
if def, ok := fd["default"]; ok {
|
||||
sectionOut[fieldKey] = def
|
||||
} else {
|
||||
sectionOut[fieldKey] = nil
|
||||
}
|
||||
}
|
||||
body[sectionKey] = sectionOut
|
||||
}
|
||||
}
|
||||
// 动态字段覆盖 — setSchemaField 按字段名自动匹配 section
|
||||
body["model"] = modelName
|
||||
setSchemaField(body, "prompt", prompt)
|
||||
if negativePrompt != "" {
|
||||
setSchemaField(body, "negative_prompt", negativePrompt)
|
||||
}
|
||||
|
||||
_, mediaTypeField, mediaURLField, _ := resolveMediaFields(videoSchema, firstFrameMapping)
|
||||
|
||||
if len(refURLs) > 0 {
|
||||
setSchemaField(body, "media", buildSchemaMedia(refURLs, mediaTypeField, mediaURLField))
|
||||
setSchemaField(body, "reference_urls", refURLs)
|
||||
// 通过 BuildSchemaRequest 构建请求体:flat input → schema 校验/填充 → 嵌套结构
|
||||
input := map[string]any{
|
||||
"prompt": prompt,
|
||||
"seed": seed,
|
||||
}
|
||||
if negativePrompt != "" {
|
||||
input["negative_prompt"] = negativePrompt
|
||||
}
|
||||
if duration > 0 {
|
||||
setSchemaField(body, "duration", duration)
|
||||
input["duration"] = duration
|
||||
}
|
||||
if seed > 0 {
|
||||
setSchemaField(body, "seed", seed)
|
||||
if len(refURLs) > 0 {
|
||||
input["media"] = buildSchemaMedia(refURLs, mediaTypeField, mediaURLField)
|
||||
refsAny := make([]any, len(refURLs))
|
||||
for i, u := range refURLs {
|
||||
refsAny[i] = u
|
||||
}
|
||||
input["reference_urls"] = refsAny
|
||||
}
|
||||
|
||||
bodyDef, _ := nested(vs, "body").(map[string]any)
|
||||
body, err := common.BuildSchemaRequest(bodyDef, input)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("build request body failed: %w", err)
|
||||
}
|
||||
if body == nil {
|
||||
body = map[string]any{}
|
||||
}
|
||||
body["model"] = modelName
|
||||
|
||||
// 统一处理首帧(segIdx==0 删除,>0 解析URL)
|
||||
prepareFirstFrame(ctx, body, segIdx, genTaskId, episodeId, dramaTitle, epTitle, firstFrameMapping, videoSchema)
|
||||
// 尺寸映射:从 drama 的 Resolution/AspectRatio 查 video_schema
|
||||
if sizes := nested(vs, "body", "parameters", "size", "sizes"); sizes != nil {
|
||||
if sm, ok := sizes.(map[string]any); ok {
|
||||
if v := resolveSizeFromSchema(sm, resolution, aspectRatio); v != "" {
|
||||
setSchemaField(body, "size", v)
|
||||
if params, ok := body["parameters"].(map[string]any); ok {
|
||||
params["size"] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user