diff --git a/short_drama.db b/short_drama.db index fb4491c..c3305d5 100644 Binary files a/short_drama.db and b/short_drama.db differ diff --git a/shortdrama/model/domain/schema_business_mapping.go b/shortdrama/model/domain/schema_business_mapping.go index 3f36598..e2d858c 100644 --- a/shortdrama/model/domain/schema_business_mapping.go +++ b/shortdrama/model/domain/schema_business_mapping.go @@ -1,11 +1,13 @@ package domain type VideoGen struct { - MinDuration string `json:"min_duration" dc:"模型支持的最小视频时长(秒)"` - MaxDuration string `json:"max_duration" dc:"模型支持的最大视频时长(秒)"` - FirstFrame string `json:"first_frame" dc:"视频的首帧/初始画面,传入一张图片作为视频第一帧画面"` - ReferenceImage string `json:"reference_image" dc:"参考图片,用于生成角色形象和风格一致性参考"` - ReferenceVideo string `json:"reference_video" dc:"参考视频"` - MaxMediaItems string `json:"max_media_items" dc:"模型允许传入的最大参考媒体数量"` - ReferenceTemplate string `json:"reference_template" dc:"prompt 中引用参考素材的标签格式,用 %d 作为编号占位符"` + MinDuration string `json:"min_duration" dc:"模型支持的最小视频时长(秒)"` + MaxDuration string `json:"max_duration" dc:"模型支持的最大视频时长(秒)"` + FirstFrame string `json:"first_frame" dc:"视频的首帧/初始画面,传入一张图片作为视频第一帧画面"` + ReferenceImage string `json:"reference_image" dc:"参考图片,用于生成角色形象和风格一致性参考"` + ReferenceVideo string `json:"reference_video" dc:"参考视频"` + MaxMediaItems string `json:"max_media_items" dc:"模型允许传入的最大参考媒体数量"` + ImgReferenceTemplate string `json:"img_reference_template" dc:"prompt 中引用参考图片的标签格式,用 %d 作为编号占位符"` + VideoReferenceTemplate string `json:"video_reference_template" dc:"prompt 中引用参考视频的标签格式,用 %d 作为编号占位符"` + AudioReferenceTemplate string `json:"audio_reference_template" dc:"prompt 中引用参考音频的标签格式,用 %d 作为编号占位符"` } diff --git a/shortdrama/service/episode_service.go b/shortdrama/service/episode_service.go index 5979d74..a77d9a4 100644 --- a/shortdrama/service/episode_service.go +++ b/shortdrama/service/episode_service.go @@ -850,14 +850,19 @@ func createPendingTasks(ctx context.Context, dramaId, epId int64, script string, var nameLabels []_nameLabel var refURLs []string - // 根据 schema 中的 media.max_items 读取 media 数组长度上限 + // 从 schema 读取 media 数组长度上限 maxRefItems := 5 - if modelCfg.SchemaMapping != "" && strings.HasPrefix(modelCfg.SchemaMapping, "{") { + if modelCfg.SchemaMapping != "" && strings.HasPrefix(modelCfg.SchemaMapping, "{") && modelCfg.Schema != "" { var m map[string]any if err := json.Unmarshal([]byte(modelCfg.SchemaMapping), &m); err == nil { - if v, ok := m["max_media_items"]; ok { - if f, ok := v.(float64); ok { - maxRefItems = int(f) + if path, ok := m["max_media_items"].(string); ok && path != "" { + var sMap map[string]any + if err := json.Unmarshal([]byte(modelCfg.Schema), &sMap); err == nil { + if v := schemaValueByPath(sMap, path); v != nil { + if f, ok := v.(float64); ok { + maxRefItems = int(f) + } + } } } } diff --git a/shortdrama/service/generation_service.go b/shortdrama/service/generation_service.go index deb3a7f..192d1d7 100644 --- a/shortdrama/service/generation_service.go +++ b/shortdrama/service/generation_service.go @@ -2441,25 +2441,51 @@ func parseMediaDef(val string) *mediaDef { return nil } -// schemaDurationBounds 从 schema_mapping 中提取 duration 在 body 中的点号路径,自动加 body 前缀后从 schema 获取实际约束值 +// schemaDurationBounds 从 schema_mapping 中读取 min_duration/max_duration 的路径, +// 再按路径从 schema JSON 中获取实际约束值。 func schemaDurationBounds(schema, schemaMapping string) (minDur, maxDur int) { var m map[string]any if err := json.Unmarshal([]byte(schemaMapping), &m); err != nil { return 0, 0 } - if v, ok := m["min_duration"]; ok { - if f, ok := v.(float64); ok { - minDur = int(f) + var sMap map[string]any + json.Unmarshal([]byte(schema), &sMap) + + if path, ok := m["min_duration"].(string); ok && path != "" { + if v := schemaValueByPath(sMap, path); v != nil { + if f, ok := v.(float64); ok { + minDur = int(f) + } } } - if v, ok := m["max_duration"]; ok { - if f, ok := v.(float64); ok { - maxDur = int(f) + if path, ok := m["max_duration"].(string); ok && path != "" { + if v := schemaValueByPath(sMap, path); v != nil { + if f, ok := v.(float64); ok { + maxDur = int(f) + } } } return } +// schemaValueByPath 按点号路径在嵌套 map 中查找值。 +// 例如 path="body.parameters.duration.min" 会依次导航。 +func schemaValueByPath(doc map[string]any, path string) any { + parts := strings.Split(path, ".") + current := any(doc) + for _, p := range parts { + m, ok := current.(map[string]any) + if !ok { + return nil + } + current = m[p] + if current == nil { + return nil + } + } + return current +} + // convertBodyMediaToBase64 将 body 中媒体文件的本地路径转为 base64(原地修改) // 覆盖 reference_urls(字符串数组)和 media(对象数组的 urlField 字段) func convertBodyMediaToBase64(body map[string]any, urlField string) { diff --git a/shortdrama/service/model_config_service.go b/shortdrama/service/model_config_service.go index cd61e30..451e108 100644 --- a/shortdrama/service/model_config_service.go +++ b/shortdrama/service/model_config_service.go @@ -114,9 +114,11 @@ func buildSchemaMapping(ctx context.Context, schema string, existingMapping stri ## 输出格式 -输出的 JSON 对象键是 json 字段名。每个字段的值由你根据 Schema 中的信息分析得出: -- 若该概念在 Schema 中有对应的具体字段,输出字段的点号路径 -- 若该概念没有直接对应的字段(如需要从字段的 description 描述中推断),输出推导出的内容 +输出的 JSON 对象键是 json 字段名。每个字段的值有两类: + +第一类(Schema 路径):若该概念在 Schema 中有直接定义位置(约束值或字段定义),输出定位到该位置的完整点号路径。若定位的是对象数组的特定元素及其属性,在路径后追加 ?实际筛选字段名=筛选值&实际值字段名=# 格式,其中 =# 标记的目标值字段名替换为 schema 中的实际字段名。 + +第二类(推导字符串):若该概念在 Schema 中没有直接对应的定义位置,输出根据 Schema 信息推导出的内容字符串。 ## 重要规则 @@ -150,12 +152,7 @@ func buildSchemaMapping(ctx context.Context, schema string, existingMapping stri return existingMapping } - content := strings.TrimSpace(result.Content) - // 去掉可能的 markdown 代码块标记 - content = strings.TrimPrefix(content, "```json") - content = strings.TrimPrefix(content, "```") - content = strings.TrimSuffix(content, "```") - content = strings.TrimSpace(content) + content := extractJSONObject(result.Content) var mapping map[string]any if err := json.Unmarshal([]byte(content), &mapping); err != nil { @@ -217,6 +214,38 @@ func schemaMaxTokens(schemaStr string) int { return 4096 } +// extractJSONObject 从字符串中提取第一个完整的 JSON 对象({...}), +// 丢弃前后的非 JSON 文本(如 LLM 返回的分析说明)。 +func extractJSONObject(s string) string { + start := strings.Index(s, "{") + if start < 0 { + return s + } + // 跳过可能出现的 markdown 代码块标记 + for start > 0 { + ch := s[start-1] + if ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' { + start-- + } else { + break + } + } + end := strings.LastIndex(s, "}") + if end <= start { + return s + } + + // 去掉可能的 markdown 代码块后缀 + snippet := s[start : end+1] + // 去掉可能的 ```json 或 ``` 前缀(已经在 { 之前被截掉了,但以防万一还留有残余) + snippet = strings.TrimPrefix(snippet, "```json") + snippet = strings.TrimPrefix(snippet, "```") + snippet = strings.TrimSuffix(snippet, "```") + snippet = strings.TrimSpace(snippet) + + return snippet +} + // buildFieldDescriptions 从 domain.VideoGen 结构体中反射读取字段定义,构建提示词中的目标字段说明 func buildFieldDescriptions() string { t := reflect.TypeOf((*domain.VideoGen)(nil)).Elem()