This commit is contained in:
2026-07-24 14:59:45 +08:00
parent 6e7d1d3e0e
commit 1635026b64
5 changed files with 302 additions and 167 deletions
+34
View File
@@ -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
}
+2 -33
View File
@@ -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
View File
@@ -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
}
+26 -77
View File
@@ -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)
}
+32 -57
View File
@@ -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
}
}
}
}