feat(*): 创作工作台 - 完善媒体上传预览与连线交互并修复任务章节及日志筛选
This commit is contained in:
1 parent
95b004ec0f
commit
068a8daea6
56 files changed
+3316
-274
No files matched your search
@@ -103,11 +103,12 @@ type AnalyticsFailureRow struct {
|
||||
|
||||
type APICallLogQuery struct {
|
||||
AnalyticsQuery
|
||||
Keyword string
|
||||
Status string
|
||||
IDs []string
|
||||
Page int
|
||||
Limit int
|
||||
RecordType string
|
||||
Keyword string
|
||||
Status string
|
||||
IDs []string
|
||||
Page int
|
||||
Limit int
|
||||
}
|
||||
|
||||
type APICallLogPage struct {
|
||||
@@ -189,8 +190,11 @@ func (s *Service) AdminAPICallLogs(actor *model.User, query APICallLogQuery) (*A
|
||||
if err := s.RequireAdmin(actor); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if query.RecordType != "" && query.RecordType != "request" && query.RecordType != "download" && query.RecordType != "all" {
|
||||
return nil, BadAuthRequest("请求明细类型无效")
|
||||
}
|
||||
filter := normalizeAnalyticsFilter(query.AnalyticsQuery)
|
||||
logs, total, err := s.repo.QueryAPICallLogs(repository.APICallLogFilter{AnalyticsFilter: filter, Keyword: query.Keyword, Status: query.Status, Page: query.Page, Limit: query.Limit})
|
||||
logs, total, err := s.repo.QueryAPICallLogs(repository.APICallLogFilter{AnalyticsFilter: filter, RecordType: query.RecordType, Keyword: query.Keyword, Status: query.Status, Page: query.Page, Limit: query.Limit})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -378,6 +382,9 @@ func (s *Service) AdminAPICallLogsCSV(actor *model.User, query APICallLogQuery)
|
||||
if err := s.RequireAdmin(actor); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if query.RecordType != "" && query.RecordType != "request" && query.RecordType != "download" && query.RecordType != "all" {
|
||||
return nil, BadAuthRequest("请求明细类型无效")
|
||||
}
|
||||
filter := normalizeAnalyticsFilter(query.AnalyticsQuery)
|
||||
ids := uniqueNonEmpty(query.IDs)
|
||||
if len(query.IDs) > 0 && len(ids) == 0 {
|
||||
@@ -386,7 +393,7 @@ func (s *Service) AdminAPICallLogsCSV(actor *model.User, query APICallLogQuery)
|
||||
if len(ids) > 200 {
|
||||
return nil, BadAuthRequest("单次最多导出 200 条已选请求明细")
|
||||
}
|
||||
logs, err := s.repo.ExportAPICallLogs(repository.APICallLogFilter{AnalyticsFilter: filter, Keyword: query.Keyword, Status: query.Status, IDs: ids}, 10_000)
|
||||
logs, err := s.repo.ExportAPICallLogs(repository.APICallLogFilter{AnalyticsFilter: filter, RecordType: query.RecordType, Keyword: query.Keyword, Status: query.Status, IDs: ids}, 10_000)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestChapterAssetsPromptContract(t *testing.T) {
|
||||
if !json.Valid([]byte(chapterAssetsJSONSchema)) {
|
||||
t.Fatal("invalid chapter assets schema")
|
||||
}
|
||||
for _, tc := range []struct {
|
||||
text string
|
||||
valid bool
|
||||
}{
|
||||
{`{"characters":[],"scenes":[{"name":"车站","description":"空旷站台","prompt":"夜间站台"}],"props":[{"name":"信封","description":"牛皮纸信封","prompt":"信封特写"}]}`, true},
|
||||
{`{"characters":[],"scenes":[],"props":[]}`, true},
|
||||
{`{"characters":[]}`, false},
|
||||
{`{"characters":[],"scenes":[{"name":"车站"}],"props":[]}`, false},
|
||||
{`{"characters":[{"name":"张三"}],"scenes":[],"props":[]}`, false},
|
||||
} {
|
||||
err := validatePromptTemplateResult(promptOperationChapterAssetsExtract, map[string]interface{}{"text": tc.text})
|
||||
if (err == nil) != tc.valid {
|
||||
t.Fatalf("valid=%v err=%v", tc.valid, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -382,13 +382,40 @@ func validatePromptTemplateResult(operation string, result map[string]interface{
|
||||
extractor := extractJSONText
|
||||
// 角色卡契约要求顶层对象,模型正文里若先出现数组(例如先列一遍角色名)会让通用提取命中错误片段,
|
||||
// 因此这里优先挑出带 characters 键的对象候选。
|
||||
if operation == promptOperationCharacterExtract {
|
||||
if operation == promptOperationCharacterExtract || operation == promptOperationChapterAssetsExtract {
|
||||
extractor = func(raw string) (string, error) { return extractPreferredJSONText(raw, "characters") }
|
||||
}
|
||||
jsonText, err := extractor(text)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s 返回内容不符合受保护 JSON 契约:%w", definition.Label, err)
|
||||
}
|
||||
if operation == promptOperationChapterAssetsExtract {
|
||||
var assets struct {
|
||||
Characters *[]map[string]interface{} `json:"characters"`
|
||||
Scenes *[]map[string]interface{} `json:"scenes"`
|
||||
Props *[]map[string]interface{} `json:"props"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(jsonText), &assets); err != nil {
|
||||
return fmt.Errorf("章节资产提取 JSON 无法解析:%w", err)
|
||||
}
|
||||
if assets.Characters == nil || assets.Scenes == nil || assets.Props == nil {
|
||||
return errors.New("章节资产提取结果必须包含 characters、scenes 和 props 数组")
|
||||
}
|
||||
for _, group := range [][]map[string]interface{}{*assets.Scenes, *assets.Props} {
|
||||
for _, asset := range group {
|
||||
for _, field := range []string{"name", "description", "prompt"} {
|
||||
value, ok := asset[field].(string)
|
||||
if !ok || strings.TrimSpace(value) == "" {
|
||||
return fmt.Errorf("章节场景或道具缺少有效的 %s", field)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(*assets.Characters) > 0 {
|
||||
return validatePromptTemplateResult(promptOperationCharacterExtract, map[string]interface{}{"text": jsonText})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if operation != promptOperationCharacterExtract {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ const (
|
||||
promptOperationStoryboardFirstFrame = "storyboard_first_frame"
|
||||
promptOperationStoryboardVideo = "storyboard_video"
|
||||
promptOperationCharacterExtract = "character_extract"
|
||||
promptOperationChapterAssetsExtract = "chapter_assets_extract"
|
||||
promptOperationCharacterTurnaround = "character_turnaround"
|
||||
)
|
||||
|
||||
@@ -18,6 +19,12 @@ const legacyStoryboardVideoPromptPreamble = "生成单一连续镜头的视频
|
||||
|
||||
func defaultPromptDefinitions() []PromptOperationDefinition {
|
||||
return []PromptOperationDefinition{
|
||||
{
|
||||
Operation: promptOperationChapterAssetsExtract, Label: "章节角色、场景与道具提取", Category: "角色", OutputType: "json", SchemaKey: "chapter-assets/v1",
|
||||
Description: "从章节正文提取角色、实际发生剧情的场景和影响剧情的道具,分别进入资产待确认列表。",
|
||||
Variables: []PromptTemplateVariable{{Label: "项目名称", Placeholder: "{{项目名称}}"}, {Label: "章节名称", Placeholder: "{{章节名称}}"}, {Label: "项目画风", Placeholder: "{{项目画风}}"}},
|
||||
DefaultContent: `你是短剧资产导演。按章节事实提取角色、场景和道具。同一身份的别名合并,不编造正文未出现的资产;未明确的视觉信息写“正文未明确”。角色包含剧情定位、稳定外貌服装体态、性格、一致性提示、三视图提示及语言、声音年龄、音色。场景描述空间布局、环境和光照;道具描述材质、形状及剧情用途。无对应资产时返回空数组。`,
|
||||
},
|
||||
{
|
||||
Operation: promptOperationStoryboardPlan, Label: "分镜规划", Category: "分镜", OutputType: "json", SchemaKey: "storyboard-plan/v3",
|
||||
Description: "把剧情、项目画风和当前角色版本规划为可执行镜头。",
|
||||
@@ -105,6 +112,13 @@ func protectedPromptContext(operation string, values map[string]string) string {
|
||||
return storyboardRepairProtectedContext(values)
|
||||
case promptOperationCharacterExtract:
|
||||
return characterExtractProtectedContext(values)
|
||||
case promptOperationChapterAssetsExtract:
|
||||
return strings.Join([]string{
|
||||
fmt.Sprintf("【任务】\n从短剧项目《%s》的章节“%s”提取角色、场景和道具。", values["项目名称"], values["章节名称"]),
|
||||
"【项目画风】\n" + values["项目画风"],
|
||||
"【章节正文】\n" + values["章节正文"],
|
||||
"【受保护输出契约】\n" + promptOutputContract(operation),
|
||||
}, "\n\n")
|
||||
case promptOperationCharacterTurnaround:
|
||||
return strings.Join([]string{"【角色名称】\n" + values["角色名称"], "【项目画风】\n" + values["项目画风"], "【角色设定】\n" + values["角色设定"]}, "\n\n")
|
||||
default:
|
||||
@@ -118,6 +132,8 @@ func promptOutputContract(operation string) string {
|
||||
return "服务端固定 JSON Schema storyboard-plan/v3(不可由运营模板或用户定制覆盖):\n" + storyboardPlanJSONSchema
|
||||
case promptOperationCharacterExtract:
|
||||
return "服务端固定 JSON Schema character-breakdown/v1(不可由运营模板或用户定制覆盖):\n" + characterBreakdownJSONSchema
|
||||
case promptOperationChapterAssetsExtract:
|
||||
return "服务端固定 JSON Schema chapter-assets/v1(不可由运营模板或用户定制覆盖):\n" + chapterAssetsJSONSchema
|
||||
default:
|
||||
return "当前操作输出普通文本提示词,没有 JSON Schema。"
|
||||
}
|
||||
@@ -182,6 +198,24 @@ const storyboardPlanJSONSchema = `{
|
||||
}
|
||||
}`
|
||||
|
||||
const chapterAssetsJSONSchema = `{
|
||||
"type": "object", "additionalProperties": false,
|
||||
"required": ["characters", "scenes", "props"],
|
||||
"properties": {
|
||||
"characters": {"$ref": "#/$defs/characterBreakdown/properties/characters"},
|
||||
"scenes": {"type": "array", "items": {"$ref": "#/$defs/asset"}},
|
||||
"props": {"type": "array", "items": {"$ref": "#/$defs/asset"}}
|
||||
},
|
||||
"$defs": {
|
||||
"asset": {
|
||||
"type": "object", "additionalProperties": false,
|
||||
"required": ["name", "description", "prompt"],
|
||||
"properties": {"name": {"type": "string"}, "description": {"type": "string"}, "prompt": {"type": "string"}}
|
||||
},
|
||||
"characterBreakdown": ` + characterBreakdownJSONSchema + `
|
||||
}
|
||||
}`
|
||||
|
||||
const characterBreakdownJSONSchema = `{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
Reference in new issue
Block a user