1
This commit is contained in:
@@ -17,7 +17,7 @@ func (c *drama) ListEpisode(ctx context.Context, req *dto.ListEpisodeReq) (res *
|
||||
}
|
||||
|
||||
func (c *drama) AddEpisode(ctx context.Context, req *dto.AddEpisodeReq) (res *dto.GetDramaRes, err error) {
|
||||
_, err = service.DramaService.AddEpisode(ctx, req.DramaId, req.Title, req.Script, req.Index)
|
||||
_, err = service.DramaService.AddEpisode(ctx, req.DramaId, req.Title, req.Description, req.Script, req.Index)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -25,13 +25,21 @@ func (c *drama) AddEpisode(ctx context.Context, req *dto.AddEpisodeReq) (res *dt
|
||||
}
|
||||
|
||||
func (c *drama) UpdateEpisode(ctx context.Context, req *dto.UpdateEpisodeReq) (res *dto.GetDramaRes, err error) {
|
||||
err = service.DramaService.UpdateEpisode(ctx, req.DramaId, req.EpId, req.Title, req.Script, req.Index)
|
||||
err = service.DramaService.UpdateEpisode(ctx, req.DramaId, req.EpId, req.Title, req.Description, req.Script, req.Index)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.Get(ctx, &dto.GetDramaReq{Id: req.DramaId})
|
||||
}
|
||||
|
||||
func (c *drama) GenerateScript(ctx context.Context, req *dto.GenerateScriptReq) (res *dto.GenerateScriptRes, err error) {
|
||||
script, err := service.DramaService.GenerateScript(ctx, req.DramaId, req.Title, req.Description)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dto.GenerateScriptRes{Script: script}, nil
|
||||
}
|
||||
|
||||
func (c *drama) DeleteEpisode(ctx context.Context, req *dto.DeleteEpisodeReq) (res *dto.GetDramaRes, err error) {
|
||||
err = service.DramaService.DeleteEpisode(ctx, req.DramaId, req.EpId)
|
||||
if err != nil {
|
||||
|
||||
@@ -39,6 +39,11 @@ func init() {
|
||||
g.Log().Warningf(ctx, "删除剧集表旧字段 %s 失败(可能已删除): %v", col, err)
|
||||
}
|
||||
}
|
||||
// 迁移:添加 description 列
|
||||
if _, err := g.DB().Exec(ctx,
|
||||
`ALTER TABLE `+public.TableNameEpisode+` ADD COLUMN description TEXT NOT NULL DEFAULT `); err != nil {
|
||||
g.Log().Debugf(ctx, "添加 description 列失败(可能已存在): %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *episodeDao) Insert(ctx context.Context, data *entity.Episode) (id int64, err error) {
|
||||
|
||||
@@ -7,20 +7,22 @@ import (
|
||||
)
|
||||
|
||||
type AddEpisodeReq struct {
|
||||
g.Meta `path:"/episode/add" method:"post" tags:"短剧管理" summary:"添加剧集"`
|
||||
DramaId int64 `json:"dramaId" dc:"短剧ID"`
|
||||
Title string `v:"required" json:"title" dc:"剧集标题"`
|
||||
Script string `json:"script" dc:"剧集脚本"`
|
||||
Index int `json:"index" dc:"剧集序号"`
|
||||
g.Meta `path:"/episode/add" method:"post" tags:"短剧管理" summary:"添加剧集"`
|
||||
DramaId int64 `json:"dramaId" dc:"短剧ID"`
|
||||
Title string `v:"required" json:"title" dc:"剧集标题"`
|
||||
Description string `json:"description" dc:"剧情描述"`
|
||||
Script string `json:"script" dc:"剧集脚本"`
|
||||
Index int `json:"index" dc:"剧集序号"`
|
||||
}
|
||||
|
||||
type UpdateEpisodeReq struct {
|
||||
g.Meta `path:"/episode/update" method:"post" tags:"短剧管理" summary:"更新剧集"`
|
||||
DramaId int64 `json:"dramaId" dc:"短剧ID"`
|
||||
EpId int64 `json:"epId" dc:"剧集ID"`
|
||||
Title string `v:"required" json:"title" dc:"剧集标题"`
|
||||
Script string `json:"script" dc:"剧集脚本"`
|
||||
Index int `json:"index" dc:"剧集序号"`
|
||||
g.Meta `path:"/episode/update" method:"post" tags:"短剧管理" summary:"更新剧集"`
|
||||
DramaId int64 `json:"dramaId" dc:"短剧ID"`
|
||||
EpId int64 `json:"epId" dc:"剧集ID"`
|
||||
Title string `v:"required" json:"title" dc:"剧集标题"`
|
||||
Description string `json:"description" dc:"剧情描述"`
|
||||
Script string `json:"script" dc:"剧集脚本"`
|
||||
Index int `json:"index" dc:"剧集序号"`
|
||||
}
|
||||
|
||||
type DeleteEpisodeReq struct {
|
||||
@@ -47,3 +49,14 @@ type EpisodePollRes struct {
|
||||
Tasks []*entity.GenerationTask `json:"tasks,omitempty" dc:"所有生成任务"`
|
||||
CurrentTaskId int64 `json:"currentTaskId,omitempty" dc:"当前审核任务ID"`
|
||||
}
|
||||
|
||||
type GenerateScriptReq struct {
|
||||
g.Meta `path:"/episode/generate-script" method:"post" tags:"短剧管理" summary:"生成剧集脚本"`
|
||||
DramaId int64 `json:"dramaId" dc:"短剧ID"`
|
||||
Title string `json:"title" dc:"剧集标题"`
|
||||
Description string `json:"description" dc:"剧情描述"`
|
||||
}
|
||||
|
||||
type GenerateScriptRes struct {
|
||||
Script string `json:"script" dc:"生成的脚本"`
|
||||
}
|
||||
|
||||
@@ -3,14 +3,15 @@ package entity
|
||||
import "github.com/gogf/gf/v2/os/gtime"
|
||||
|
||||
type Episode struct {
|
||||
Id int64 `orm:"id" json:"id" dc:"剧集ID"`
|
||||
DramaId int64 `orm:"drama_id" json:"dramaId" dc:"短剧ID"`
|
||||
Index int `orm:"idx" json:"index" dc:"剧集序号"`
|
||||
Title string `orm:"title" json:"title" dc:"剧集标题"`
|
||||
Script string `orm:"script" json:"script" dc:"剧集脚本"`
|
||||
Status string `orm:"status" json:"status" dc:"生成状态"`
|
||||
VideoUrl string `orm:"video_url" json:"videoUrl" dc:"视频URL"`
|
||||
CreatedAt *gtime.Time `orm:"created_at" json:"createdAt" dc:"创建时间"`
|
||||
UpdatedAt *gtime.Time `orm:"updated_at" json:"updatedAt" dc:"更新时间"`
|
||||
DeletedAt *gtime.Time `orm:"deleted_at" json:"deletedAt" dc:"删除时间"`
|
||||
Id int64 `orm:"id" json:"id" dc:"剧集ID"`
|
||||
DramaId int64 `orm:"drama_id" json:"dramaId" dc:"短剧ID"`
|
||||
Index int `orm:"idx" json:"index" dc:"剧集序号"`
|
||||
Title string `orm:"title" json:"title" dc:"剧集标题"`
|
||||
Description string `orm:"description" json:"description" dc:"剧情描述"`
|
||||
Script string `orm:"script" json:"script" dc:"剧集脚本"`
|
||||
Status string `orm:"status" json:"status" dc:"生成状态"`
|
||||
VideoUrl string `orm:"video_url" json:"videoUrl" dc:"视频URL"`
|
||||
CreatedAt *gtime.Time `orm:"created_at" json:"createdAt" dc:"创建时间"`
|
||||
UpdatedAt *gtime.Time `orm:"updated_at" json:"updatedAt" dc:"更新时间"`
|
||||
DeletedAt *gtime.Time `orm:"deleted_at" json:"deletedAt" dc:"删除时间"`
|
||||
}
|
||||
|
||||
@@ -25,3 +25,10 @@ type SegmentCharacter struct {
|
||||
Description string `json:"description"`
|
||||
ImageBase64 string `json:"-"` // 形象 base64(不序列化到 StepsData)
|
||||
}
|
||||
|
||||
// VideoRef 视频模型 API 参考素材
|
||||
type VideoRef struct {
|
||||
Type string `json:"type"` // "character" | "scene" | "prop"
|
||||
Name string `json:"name"` // 实体名称
|
||||
MediaURL string `json:"mediaUrl"` // 参考素材 URL(base64 data URL 或 HTTP URL)
|
||||
}
|
||||
|
||||
@@ -198,13 +198,19 @@ func (s *dramaService) GenerateEpisode(ctx context.Context, dramaId, epId int64,
|
||||
}
|
||||
// 默认使用并行生成模式
|
||||
mode = "parallel"
|
||||
// 预加载当前短剧的演员/场景/道具,构建引用索引
|
||||
genCtx2, err := BuildGenerationContext(context.Background(), d)
|
||||
if err != nil {
|
||||
return fmt.Errorf("构建生成上下文失败: %w", err)
|
||||
}
|
||||
g.Log().Infof(ctx, "预加载引用数据: %d个演员, %d个场景, %d个道具, %d个引用",
|
||||
len(genCtx2.Characters), len(genCtx2.Scenes), len(genCtx2.Props), len(genCtx2.OrderedRefs))
|
||||
|
||||
genCtx := agent.WithModelConfig(context.Background(), &agent.ModelConfig{
|
||||
APIKey: modelCfg.ChatApiKey,
|
||||
BaseURL: modelCfg.ChatBaseUrl,
|
||||
})
|
||||
genCtx = agent.WithDramaId(genCtx, d.Id)
|
||||
genCtx = agent.WithDramaId(genCtx, d.Id)
|
||||
|
||||
segDurs := calcSegDurs(d.EpisodeDuration, modelCfg)
|
||||
numSegments := len(segDurs)
|
||||
@@ -257,7 +263,7 @@ func (s *dramaService) GenerateEpisode(ctx context.Context, dramaId, epId int64,
|
||||
for i, segDur := range segDurs {
|
||||
taskId := taskIds[i]
|
||||
g.Log().Infof(genCtx, "第%d集第%d段开始串行生成(segDur=%ds)", ep.Index, i+1, segDur)
|
||||
if err := s.generateOneSegment(genCtx, d, ep, taskId, i, segDur, ""); err != nil {
|
||||
if err := s.generateOneSegment(genCtx, d, ep, taskId, i, segDur, "", genCtx2); err != nil {
|
||||
g.Log().Errorf(genCtx, "第%d集第%d段串行生成失败: %v", ep.Index, i+1, err)
|
||||
_ = dao.GenerationTask.UpdateFailed(genCtx, taskId, err.Error())
|
||||
clearPollCache(genCtx, epId)
|
||||
@@ -284,7 +290,7 @@ func (s *dramaService) GenerateEpisode(ctx context.Context, dramaId, epId int64,
|
||||
go func(idx, dur int, tid int64) {
|
||||
startTime := time.Now()
|
||||
g.Log().Infof(genCtx, "第%d集第%d段开始生成(segDur=%ds)", ep.Index, idx+1, dur)
|
||||
if err := s.generateOneSegment(genCtx, d, ep, tid, idx, dur, ""); err != nil {
|
||||
if err := s.generateOneSegment(genCtx, d, ep, tid, idx, dur, "", genCtx2); err != nil {
|
||||
g.Log().Errorf(genCtx, "第%d集第%d段生成失败(耗时%v): %v", ep.Index, idx+1, time.Since(startTime), err)
|
||||
_ = dao.GenerationTask.UpdateFailed(genCtx, tid, err.Error())
|
||||
clearPollCache(genCtx, epId)
|
||||
@@ -303,13 +309,11 @@ 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) error {
|
||||
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)
|
||||
|
||||
// 加载场景/道具/演员
|
||||
characters, _, _ := dao.Character.ListPageByDrama(ctx, d.Id, 1, -1)
|
||||
scenes, _ := dao.Scene.ListByDrama(ctx, d.Id)
|
||||
props, _ := dao.Prop.ListByDrama(ctx, d.Id)
|
||||
// 使用预加载的场景/道具/演员
|
||||
characters := genCtx.Characters
|
||||
|
||||
// 计算本段在整集中的起始时间(累计前几段时长)
|
||||
segDurs := calcSegDurs(d.EpisodeDuration, modelCfg)
|
||||
@@ -320,7 +324,7 @@ func (s *dramaService) generateOneSegment(ctx context.Context, d *entity.Drama,
|
||||
totalSegs := len(segDurs)
|
||||
|
||||
// 调用 Agent
|
||||
result, err := s.generateSegment(ctx, d, ep, segIdx, segDur, segStartTime, totalSegs, modelCfg, feedback, characters, scenes, props)
|
||||
result, err := s.generateSegment(ctx, d, ep, segIdx, segDur, segStartTime, totalSegs, modelCfg, feedback, genCtx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -379,17 +383,6 @@ func (s *dramaService) generateOneSegment(ctx context.Context, d *entity.Drama,
|
||||
}
|
||||
}
|
||||
|
||||
// 保存脚本到 workspace
|
||||
var scriptPath string
|
||||
if segOutput.TextOutput != "" {
|
||||
scriptName := fmt.Sprintf("第%d集第%d段(%ds).md", ep.Index, segIdx+1, segDur)
|
||||
if path, err := s.saveTextFile(ctx, dramaTitle, segOutput.TextOutput, "脚本", scriptName); err == nil {
|
||||
scriptPath = path
|
||||
} else {
|
||||
g.Log().Warningf(ctx, "保存脚本文件失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 读取 task 中已有的 NumSegments
|
||||
task, _ := dao.GenerationTask.GetOne(ctx, taskId)
|
||||
numSegments := 1
|
||||
@@ -398,7 +391,16 @@ func (s *dramaService) generateOneSegment(ctx context.Context, d *entity.Drama,
|
||||
}
|
||||
|
||||
// 提交视频合成
|
||||
taskID, submitErr := s.submitVideoTask(ctx, d, ep, segIdx, segDur, segOutput.Scenes)
|
||||
// 构建引用列表:从Agent输出的角色名匹配预加载的参考图片
|
||||
var videoRefs []model.VideoRef
|
||||
for _, ch := range segOutput.Characters {
|
||||
if url := genCtx.LookupRef("演员", ch.Name); url != "" {
|
||||
videoRefs = append(videoRefs, model.VideoRef{
|
||||
Type: "character", Name: ch.Name, MediaURL: url,
|
||||
})
|
||||
}
|
||||
}
|
||||
taskID, submitErr := s.submitVideoTask(ctx, d, ep, segIdx, segDur, segOutput.Scenes, videoRefs)
|
||||
if submitErr != nil {
|
||||
g.Log().Warningf(ctx, "第%d段视频提交失败,后台轮询器将自动重试: %v", segIdx+1, submitErr)
|
||||
} else {
|
||||
@@ -410,9 +412,6 @@ func (s *dramaService) generateOneSegment(ctx context.Context, d *entity.Drama,
|
||||
"num_segments": numSegments,
|
||||
"updated_at": nil,
|
||||
}
|
||||
if scriptPath != "" {
|
||||
updateFields["script_path"] = scriptPath
|
||||
}
|
||||
if taskID != "" {
|
||||
updateFields["video_task_id"] = taskID
|
||||
}
|
||||
@@ -424,7 +423,7 @@ func (s *dramaService) generateOneSegment(ctx context.Context, d *entity.Drama,
|
||||
// generateSegment 调用 Agent 生成一段内容
|
||||
func (s *dramaService) generateSegment(ctx context.Context, d *entity.Drama, ep *entity.Episode,
|
||||
segIdx, segDur, segStartTime, totalSegs int, modelCfg *entity.ModelConfig, feedback string,
|
||||
characters []*entity.Character, scenes []*entity.Scene, props []*entity.Prop) (string, error) {
|
||||
genCtx *GenerationContext) (string, error) {
|
||||
|
||||
chatModel, err := agent.NewChatModel(ctx, &agent.ModelConfig{
|
||||
ModelName: modelCfg.ChatModelName,
|
||||
@@ -438,7 +437,7 @@ func (s *dramaService) generateSegment(ctx context.Context, d *entity.Drama, ep
|
||||
}
|
||||
|
||||
systemPrompt := s.buildSegPrompt(ctx, d, ep, segIdx, segDur, totalSegs, modelCfg)
|
||||
userInput := s.buildSegUserInput(d, ep, segIdx, segDur, segStartTime, totalSegs, feedback, characters, scenes, props, modelCfg)
|
||||
userInput := s.buildSegUserInput(d, ep, segIdx, segDur, segStartTime, totalSegs, feedback, genCtx, modelCfg)
|
||||
|
||||
// 估算输入 token 数(中文约 1.5 字符/token),动态调整 max_tokens
|
||||
inputChars := len([]rune(systemPrompt)) + len([]rune(userInput))
|
||||
@@ -522,7 +521,7 @@ func (s *dramaService) buildSegPrompt(ctx context.Context, d *entity.Drama, ep *
|
||||
)
|
||||
}
|
||||
|
||||
func (s *dramaService) buildSegUserInput(d *entity.Drama, ep *entity.Episode, segIdx, segDur, segStartTime, totalSegs int, feedback string, characters []*entity.Character, scenes []*entity.Scene, props []*entity.Prop, modelCfg *entity.ModelConfig) string {
|
||||
func (s *dramaService) buildSegUserInput(d *entity.Drama, ep *entity.Episode, segIdx, segDur, segStartTime, totalSegs int, feedback string, genCtx *GenerationContext, modelCfg *entity.ModelConfig) string {
|
||||
feedbackText := ""
|
||||
if feedback != "" {
|
||||
feedbackText = fmt.Sprintf("\n【用户反馈】\n%s\n请根据以上反馈调整本段内容。", feedback)
|
||||
@@ -530,18 +529,23 @@ func (s *dramaService) buildSegUserInput(d *entity.Drama, ep *entity.Episode, se
|
||||
|
||||
// 演员信息
|
||||
charText := ""
|
||||
if len(characters) > 0 {
|
||||
if len(genCtx.Characters) > 0 {
|
||||
var charLines []string
|
||||
for _, c := range characters {
|
||||
for _, c := range genCtx.Characters {
|
||||
voiceDesc := c.VoicePath
|
||||
if voiceDesc == "" {
|
||||
voiceDesc = "有配音(未指定文件)"
|
||||
}
|
||||
refIdx := genCtx.FindRefIndex("演员", c.Name)
|
||||
refIdxStr := ""
|
||||
if refIdx >= 0 {
|
||||
refIdxStr = fmt.Sprintf(",参考图索引:%d", refIdx)
|
||||
}
|
||||
portraitInfo := ""
|
||||
if c.PortraitPath != "" {
|
||||
portraitInfo = fmt.Sprintf(",形象参考:%s", c.PortraitPath)
|
||||
}
|
||||
info := fmt.Sprintf("- 名称:%s,描述:%s,声音:%s%s", c.Name, c.Description, voiceDesc, portraitInfo)
|
||||
info := fmt.Sprintf("- 名称:%s,描述:%s,声音:%s%s%s", c.Name, c.Description, voiceDesc, portraitInfo, refIdxStr)
|
||||
charLines = append(charLines, info)
|
||||
}
|
||||
charText = "\n【演员信息】\n" + strings.Join(charLines, "\n")
|
||||
@@ -551,14 +555,19 @@ func (s *dramaService) buildSegUserInput(d *entity.Drama, ep *entity.Episode, se
|
||||
|
||||
// 场景信息
|
||||
sceneText := ""
|
||||
if len(scenes) > 0 {
|
||||
if len(genCtx.Scenes) > 0 {
|
||||
var sceneLines []string
|
||||
for _, sc := range scenes {
|
||||
for _, sc := range genCtx.Scenes {
|
||||
refIdx := genCtx.FindRefIndex("场景", sc.Name)
|
||||
refIdxStr := ""
|
||||
if refIdx >= 0 {
|
||||
refIdxStr = fmt.Sprintf(",参考图索引:%d", refIdx)
|
||||
}
|
||||
imgInfo := ""
|
||||
if sc.ImagePath != "" {
|
||||
imgInfo = fmt.Sprintf(",图片路径:%s", sc.ImagePath)
|
||||
}
|
||||
sceneLines = append(sceneLines, fmt.Sprintf("- 名称:%s,描述:%s%s", sc.Name, sc.Description, imgInfo))
|
||||
sceneLines = append(sceneLines, fmt.Sprintf("- 名称:%s,描述:%s%s%s", sc.Name, sc.Description, imgInfo, refIdxStr))
|
||||
}
|
||||
sceneText = "\n【可用场景】\n" + strings.Join(sceneLines, "\n")
|
||||
} else {
|
||||
@@ -567,14 +576,19 @@ func (s *dramaService) buildSegUserInput(d *entity.Drama, ep *entity.Episode, se
|
||||
|
||||
// 道具信息
|
||||
propText := ""
|
||||
if len(props) > 0 {
|
||||
if len(genCtx.Props) > 0 {
|
||||
var propLines []string
|
||||
for _, p := range props {
|
||||
for _, p := range genCtx.Props {
|
||||
refIdx := genCtx.FindRefIndex("道具", p.Name)
|
||||
refIdxStr := ""
|
||||
if refIdx >= 0 {
|
||||
refIdxStr = fmt.Sprintf(",参考图索引:%d", refIdx)
|
||||
}
|
||||
imgInfo := ""
|
||||
if p.ImagePath != "" {
|
||||
imgInfo = fmt.Sprintf(",图片路径:%s", p.ImagePath)
|
||||
}
|
||||
propLines = append(propLines, fmt.Sprintf("- 名称:%s,描述:%s%s", p.Name, p.Description, imgInfo))
|
||||
propLines = append(propLines, fmt.Sprintf("- 名称:%s,描述:%s%s%s", p.Name, p.Description, imgInfo, refIdxStr))
|
||||
}
|
||||
propText = "\n【可用道具】\n" + strings.Join(propLines, "\n")
|
||||
} else {
|
||||
@@ -685,6 +699,13 @@ func (s *dramaService) FeedbackSegment(ctx context.Context, taskId int64, feedba
|
||||
segDurs := calcSegDurs(d.EpisodeDuration, modelCfg)
|
||||
segDur := segDurs[task.SegmentIdx]
|
||||
|
||||
// 预加载引用数据
|
||||
feedbackGenCtx, fbErr := BuildGenerationContext(context.Background(), d)
|
||||
if fbErr != nil {
|
||||
g.Log().Errorf(ctx, "反馈重试构建生成上下文失败: %v", fbErr)
|
||||
return fbErr
|
||||
}
|
||||
|
||||
go func() {
|
||||
genCtx := context.Background()
|
||||
genCtx = agent.WithModelConfig(genCtx, &agent.ModelConfig{
|
||||
@@ -693,7 +714,7 @@ func (s *dramaService) FeedbackSegment(ctx context.Context, taskId int64, feedba
|
||||
})
|
||||
genCtx = agent.WithDramaId(genCtx, d.Id)
|
||||
g.Log().Infof(genCtx, "第%d集第%d段根据反馈重新生成", ep.Index, task.SegmentIdx+1)
|
||||
if err := s.generateOneSegment(genCtx, d, ep, taskId, task.SegmentIdx, segDur, feedback); err != nil {
|
||||
if err := s.generateOneSegment(genCtx, d, ep, taskId, task.SegmentIdx, segDur, feedback, feedbackGenCtx); err != nil {
|
||||
g.Log().Errorf(genCtx, "第%d集第%d段重新生成失败: %v", ep.Index, task.SegmentIdx+1, err)
|
||||
_ = dao.GenerationTask.UpdateFailed(genCtx, taskId, err.Error())
|
||||
clearPollCache(genCtx, task.EpisodeId)
|
||||
@@ -876,35 +897,6 @@ func (s *dramaService) pollPendingVideos(ctx context.Context) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 没有 VideoTaskId — 尝试重新提交
|
||||
if task.ScriptPath == "" {
|
||||
continue
|
||||
}
|
||||
d, _ := dao.Drama.GetOne(ctx, task.DramaId)
|
||||
if d == nil {
|
||||
continue
|
||||
}
|
||||
ep, _ := dao.Episode.GetOne(ctx, task.EpisodeId)
|
||||
if ep == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 从脚本文件中读取 Agent 输出,解析场景用于视频重试
|
||||
textBytes, readErr := os.ReadFile(task.ScriptPath)
|
||||
if readErr != nil {
|
||||
g.Log().Warningf(ctx, "轮询器: 任务 %d 读取脚本文件失败: %v", task.Id, readErr)
|
||||
continue
|
||||
}
|
||||
segOutput := model.ParseAgentOutput(string(textBytes), task.SegmentIdx)
|
||||
// 轮询器重试时不携带 duration(初次提交已在 generateOneSegment 中携带,避免 async 任务再次因 duration 不支持而失败)
|
||||
taskID, submitErr := s.submitVideoTask(ctx, d, ep, task.SegmentIdx, 0, segOutput.Scenes)
|
||||
if submitErr != nil {
|
||||
g.Log().Warningf(ctx, "轮询器: 任务 %d 第%d段视频重试提交失败: %v", task.Id, task.SegmentIdx+1, submitErr)
|
||||
continue
|
||||
}
|
||||
_ = dao.GenerationTask.UpdateFields(ctx, task.Id, g.Map{"video_task_id": taskID})
|
||||
g.Log().Infof(ctx, "轮询器: 任务 %d 第%d段视频重试提交成功: %s", task.Id, task.SegmentIdx+1, taskID)
|
||||
epUpdates[task.EpisodeId] = true
|
||||
}
|
||||
|
||||
// 刷新所有 generating 任务的 episode 缓存(不管状态有无变化,确保前端轮询不走 DB)
|
||||
@@ -984,7 +976,7 @@ func (s *dramaService) pollVideoTaskOnce(ctx context.Context, modelCfg *entity.M
|
||||
// ==================== Video Generation ====================
|
||||
|
||||
// submitVideoTask 提交视频合成任务(duration 使用指定的 segDur,不再硬编码)
|
||||
func (s *dramaService) submitVideoTask(ctx context.Context, d *entity.Drama, ep *entity.Episode, segIdx, segDur int, scenes []model.SegmentScene) (string, error) {
|
||||
func (s *dramaService) submitVideoTask(ctx context.Context, d *entity.Drama, ep *entity.Episode, segIdx, segDur int, scenes []model.SegmentScene, refs []model.VideoRef) (string, error) {
|
||||
modelCfg := ConfigService.Get(ctx)
|
||||
if modelCfg.VideoApiKey == "" || modelCfg.VideoBaseUrl == "" || modelCfg.VideoModelName == "" {
|
||||
return "", fmt.Errorf("视频模型未配置")
|
||||
@@ -1008,7 +1000,23 @@ func (s *dramaService) submitVideoTask(ctx context.Context, d *entity.Drama, ep
|
||||
|
||||
// 视频模型输入文本有长度限制,根据模型配置中的 max_tokens 决定截断长度
|
||||
sceneText := strings.Join(sceneDescs, ";")
|
||||
prompt := fmt.Sprintf("短剧《%s》第%d集第%d段:%s", d.Title, ep.Index, segIdx+1, sceneText)
|
||||
|
||||
// 构建参考素材说明
|
||||
var refImages []string
|
||||
refPrompt := ""
|
||||
if len(refs) > 0 {
|
||||
refImages = make([]string, 0, len(refs))
|
||||
var refParts []string
|
||||
for _, r := range refs {
|
||||
refParts = append(refParts, fmt.Sprintf("%s(%s)", r.Name, r.Type))
|
||||
if r.MediaURL != "" {
|
||||
refImages = append(refImages, r.MediaURL)
|
||||
}
|
||||
}
|
||||
refPrompt = fmt.Sprintf(",参考素材:%s", strings.Join(refParts, "、"))
|
||||
}
|
||||
|
||||
prompt := fmt.Sprintf("短剧《%s》第%d集第%d段:%s%s", d.Title, ep.Index, segIdx+1, sceneText, refPrompt)
|
||||
maxInputLen := 3000
|
||||
if modelCfg.MaxTokens > 0 {
|
||||
maxInputLen = modelCfg.MaxTokens
|
||||
@@ -1030,9 +1038,15 @@ func (s *dramaService) submitVideoTask(ctx context.Context, d *entity.Drama, ep
|
||||
|
||||
body := map[string]any{
|
||||
"model": modelCfg.VideoModelName,
|
||||
"input": map[string]any{
|
||||
"prompt": prompt,
|
||||
},
|
||||
"input": func() map[string]any {
|
||||
m := map[string]any{
|
||||
"prompt": prompt,
|
||||
}
|
||||
if len(refImages) > 0 {
|
||||
m["images"] = refImages
|
||||
}
|
||||
return m
|
||||
}(),
|
||||
"parameters": map[string]any{
|
||||
"size": "720*1280",
|
||||
},
|
||||
@@ -1350,21 +1364,10 @@ func (s *dramaService) copyFile(src, dst string) error {
|
||||
|
||||
// ==================== Helpers ====================
|
||||
|
||||
// cleanupEpisodeWorkspace 清理本集之前生成的脚本和视频文件
|
||||
// cleanupEpisodeWorkspace 清理本集之前生成的视频文件
|
||||
func cleanupEpisodeWorkspace(ctx context.Context, dramaTitle string, epIndex int, epTitle string) {
|
||||
wsDir := WorkspaceDir(dramaTitle)
|
||||
|
||||
// 清理脚本文件:第{epIndex}集*.md
|
||||
scriptsDir := filepath.Join(wsDir, "脚本")
|
||||
if entries, err := os.ReadDir(scriptsDir); err == nil {
|
||||
prefix := fmt.Sprintf("第%d集", epIndex)
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() && strings.HasPrefix(e.Name(), prefix) && strings.HasSuffix(e.Name(), ".md") {
|
||||
_ = os.Remove(filepath.Join(scriptsDir, e.Name()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 清理产出视频文件
|
||||
videoDir := filepath.Join(wsDir, "产出视频")
|
||||
if entries, err := os.ReadDir(videoDir); err == nil {
|
||||
@@ -1457,20 +1460,6 @@ func (s *dramaService) saveBase64Image(ctx context.Context, dramaTitle, b64Data,
|
||||
return dest, nil
|
||||
}
|
||||
|
||||
// saveTextFile 将文本内容保存到 workspace 目录,返回绝对路径
|
||||
func (s *dramaService) saveTextFile(ctx context.Context, dramaTitle, content, subDir, fileName string) (string, error) {
|
||||
wsDir := WorkspaceDir(dramaTitle)
|
||||
subPath := filepath.Join(wsDir, subDir)
|
||||
if err := os.MkdirAll(subPath, 0755); err != nil {
|
||||
return "", fmt.Errorf("创建目录失败: %w", err)
|
||||
}
|
||||
dest := filepath.Join(subPath, fileName)
|
||||
if err := os.WriteFile(dest, []byte(content), 0644); err != nil {
|
||||
return "", fmt.Errorf("写入文件失败: %w", err)
|
||||
}
|
||||
return dest, nil
|
||||
}
|
||||
|
||||
// downloadToLocal 下载远程文件到本地
|
||||
func (s *dramaService) downloadToLocal(ctx context.Context, url, dramaTitle, subDir, fileName string) (string, error) {
|
||||
wsDir := WorkspaceDir(dramaTitle)
|
||||
|
||||
@@ -7,17 +7,19 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"video-factory/shortdrama/agent"
|
||||
"video-factory/shortdrama/consts/public"
|
||||
"video-factory/shortdrama/dao"
|
||||
"video-factory/shortdrama/model/entity"
|
||||
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
// ==================== Episode CRUD ====================
|
||||
|
||||
func (s *dramaService) AddEpisode(ctx context.Context, dramaId int64, title, script string, index int) (int64, error) {
|
||||
func (s *dramaService) AddEpisode(ctx context.Context, dramaId int64, title, description, script string, index int) (int64, error) {
|
||||
if index <= 0 {
|
||||
maxIdx, err := dao.Episode.GetMaxIndex(ctx, dramaId)
|
||||
if err != nil {
|
||||
@@ -26,28 +28,20 @@ func (s *dramaService) AddEpisode(ctx context.Context, dramaId int64, title, scr
|
||||
index = maxIdx + 1
|
||||
}
|
||||
id, err := dao.Episode.Insert(ctx, &entity.Episode{
|
||||
DramaId: dramaId,
|
||||
Index: index,
|
||||
Title: title,
|
||||
Script: script,
|
||||
DramaId: dramaId,
|
||||
Index: index,
|
||||
Title: title,
|
||||
Description: description,
|
||||
Script: script,
|
||||
Status: "pending",
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
// 保存剧集描述到 workspace 文件
|
||||
if script != "" {
|
||||
if d, _ := dao.Drama.GetOne(ctx, dramaId); d != nil {
|
||||
safeName := sanitizeDirName(title)
|
||||
fileName := fmt.Sprintf("%s.md", safeName)
|
||||
if _, err := s.saveTextFile(ctx, d.Title, script, "剧集描述", fileName); err != nil {
|
||||
g.Log().Warningf(ctx, "保存剧集描述文件失败: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (s *dramaService) UpdateEpisode(ctx context.Context, dramaId, epId int64, title, script string, index int) error {
|
||||
func (s *dramaService) UpdateEpisode(ctx context.Context, dramaId, epId int64, title, description, script string, index int) error {
|
||||
e, err := dao.Episode.GetOne(ctx, epId)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -58,8 +52,14 @@ func (s *dramaService) UpdateEpisode(ctx context.Context, dramaId, epId int64, t
|
||||
if title != "" {
|
||||
e.Title = title
|
||||
}
|
||||
if description != "" {
|
||||
e.Description = description
|
||||
}
|
||||
if script != "" {
|
||||
e.Script = script
|
||||
if e.Status == "" {
|
||||
e.Status = "pending"
|
||||
}
|
||||
}
|
||||
if index > 0 {
|
||||
e.Index = index
|
||||
@@ -67,16 +67,6 @@ func (s *dramaService) UpdateEpisode(ctx context.Context, dramaId, epId int64, t
|
||||
if err := dao.Episode.Update(ctx, epId, e); err != nil {
|
||||
return err
|
||||
}
|
||||
// 保存剧集描述到 workspace 文件
|
||||
if script != "" {
|
||||
if d, _ := dao.Drama.GetOne(ctx, dramaId); d != nil {
|
||||
safeName := sanitizeDirName(e.Title)
|
||||
fileName := fmt.Sprintf("%s.md", safeName)
|
||||
if _, err := s.saveTextFile(ctx, d.Title, script, "剧集描述", fileName); err != nil {
|
||||
g.Log().Warningf(ctx, "保存剧集描述文件失败: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -86,15 +76,6 @@ func (s *dramaService) DeleteEpisode(ctx context.Context, dramaId, epId int64) e
|
||||
return err
|
||||
}
|
||||
|
||||
// 先收集脚本路径和剧集标题(在删除前读取,用于后续文件清理)
|
||||
tasks, _ := dao.GenerationTask.ListByEpisodeIds(ctx, []int64{epId})
|
||||
scriptPaths := make([]string, 0, len(tasks))
|
||||
for _, t := range tasks {
|
||||
if t.ScriptPath != "" {
|
||||
scriptPaths = append(scriptPaths, t.ScriptPath)
|
||||
}
|
||||
}
|
||||
|
||||
var eTitle string
|
||||
if d != nil {
|
||||
if e, _ := dao.Episode.GetOne(ctx, epId); e != nil {
|
||||
@@ -119,8 +100,6 @@ func (s *dramaService) DeleteEpisode(ctx context.Context, dramaId, epId int64) e
|
||||
workspaceDir := WorkspaceDir(d.Title)
|
||||
safeEp := sanitizeDirName(eTitle)
|
||||
|
||||
os.Remove(filepath.Join(workspaceDir, "剧集描述", safeEp+".md"))
|
||||
|
||||
// 清理产出视频目录中本集的所有文件
|
||||
videoDir := filepath.Join(workspaceDir, "产出视频")
|
||||
if entries, err := os.ReadDir(videoDir); err == nil {
|
||||
@@ -135,9 +114,105 @@ func (s *dramaService) DeleteEpisode(ctx context.Context, dramaId, epId int64) e
|
||||
}
|
||||
}
|
||||
|
||||
for _, p := range scriptPaths {
|
||||
os.Remove(p)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ==================== Script Generation ====================
|
||||
|
||||
// GenerateScript 使用AI为指定短剧的剧集生成脚本
|
||||
func (s *dramaService) GenerateScript(ctx context.Context, dramaId int64, episodeTitle, description string) (string, error) {
|
||||
d, err := dao.Drama.GetOne(ctx, dramaId)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if d == nil {
|
||||
return "", fmt.Errorf("短剧不存在")
|
||||
}
|
||||
|
||||
modelCfg := ConfigService.Get(ctx)
|
||||
if modelCfg.ChatApiKey == "" || modelCfg.ChatModelName == "" {
|
||||
return "", fmt.Errorf("模型未配置")
|
||||
}
|
||||
|
||||
// 加载演员/场景/道具上下文
|
||||
genCtx, err := BuildGenerationContext(ctx, d)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("加载短剧上下文失败: %w", err)
|
||||
}
|
||||
|
||||
// 创建聊天模型
|
||||
chatModel, err := agent.NewChatModel(ctx, &agent.ModelConfig{
|
||||
ModelName: modelCfg.ChatModelName,
|
||||
APIKey: modelCfg.ChatApiKey,
|
||||
BaseURL: modelCfg.ChatBaseUrl,
|
||||
MaxTokens: modelCfg.MaxTokens,
|
||||
Temperature: float32(modelCfg.Temperature),
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
systemPrompt := "你是一位专业的短剧编剧。请根据短剧信息和可用演员、场景、道具,创作一份详细的单集剧本。"
|
||||
userInput := s.buildScriptGenUserInput(d, episodeTitle, description, genCtx)
|
||||
|
||||
messages := []*schema.Message{
|
||||
schema.SystemMessage(systemPrompt),
|
||||
schema.UserMessage(userInput),
|
||||
}
|
||||
result, err := chatModel.Generate(ctx, messages)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("生成脚本失败: %w", err)
|
||||
}
|
||||
|
||||
script := result.Content
|
||||
if script == "" {
|
||||
return "", fmt.Errorf("生成的脚本为空")
|
||||
}
|
||||
|
||||
g.Log().Infof(ctx, "脚本生成成功: 短剧=%s, 剧集=%s, 长度=%d字符", d.Title, episodeTitle, len([]rune(script)))
|
||||
return script, nil
|
||||
}
|
||||
|
||||
// buildScriptGenUserInput 构建脚本生成的用户输入提示
|
||||
func (s *dramaService) buildScriptGenUserInput(d *entity.Drama, episodeTitle, description string, genCtx *GenerationContext) string {
|
||||
var b strings.Builder
|
||||
|
||||
fmt.Fprintf(&b, "【短剧信息】\n标题:%s\n风格:%s\n每集时长:%d秒\n\n", d.Title, d.Style, d.EpisodeDuration)
|
||||
|
||||
if episodeTitle != "" {
|
||||
fmt.Fprintf(&b, "【目标剧集】\n%s\n\n", episodeTitle)
|
||||
}
|
||||
|
||||
if description != "" {
|
||||
fmt.Fprintf(&b, "【剧情描述】\n%s\n\n", description)
|
||||
}
|
||||
|
||||
if len(genCtx.Characters) > 0 {
|
||||
b.WriteString("【可用演员】\n")
|
||||
for _, c := range genCtx.Characters {
|
||||
fmt.Fprintf(&b, "- %s:%s\n", c.Name, c.Description)
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
if len(genCtx.Scenes) > 0 {
|
||||
b.WriteString("【可用场景】\n")
|
||||
for _, sc := range genCtx.Scenes {
|
||||
fmt.Fprintf(&b, "- %s:%s\n", sc.Name, sc.Description)
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
if len(genCtx.Props) > 0 {
|
||||
b.WriteString("【可用道具】\n")
|
||||
for _, p := range genCtx.Props {
|
||||
fmt.Fprintf(&b, "- %s\n", p.Name)
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
b.WriteString(fmt.Sprintf("【要求】\n请为上述剧集创作一份详细的剧本。剧本必须按以下格式:\n\n本集标题:[标题]\n\n【场景1:[场景名称]】\n[场景详细描述,包括环境、时间、氛围等]\n\n[演员名]:[台词]\n[演员名]:[台词]\n\n【场景2:[场景名称]】\n...\n\n要求:\n1. 剧本包含具体的场景描述和人物对话\n2. 所有场景时长之和应等于 %d 秒\n3. 优先使用提供的演员、场景和道具,如需新增请合理创作\n4. 对话自然流畅,情节有起伏\n5. 场景数量建议3-8个\n", d.EpisodeDuration))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"video-factory/shortdrama/dao"
|
||||
"video-factory/shortdrama/model/entity"
|
||||
)
|
||||
|
||||
// RefIndex 引用索引:{"演员":{"张三":"data:image/...;base64,..."}, "场景":{"..."}, "道具":{"..."}}
|
||||
type RefIndex map[string]map[string]string
|
||||
|
||||
// OrderedRef 带索引的引用,用于提示词中描述"张三使用参考图第N张"
|
||||
type OrderedRef struct {
|
||||
Category string // "演员" | "场景" | "道具"
|
||||
Name string
|
||||
URL string // base64 data URL
|
||||
Index int // 在 OrderedRefs 中的位置
|
||||
}
|
||||
|
||||
// GenerationContext 一次生成会话的上下文,预加载当前短剧的演员/场景/道具数据
|
||||
type GenerationContext struct {
|
||||
Drama *entity.Drama
|
||||
Characters []*entity.Character
|
||||
Scenes []*entity.Scene
|
||||
Props []*entity.Prop
|
||||
|
||||
// RefIndex 按名称索引,用于 Agent 输出匹配
|
||||
RefIndex RefIndex
|
||||
|
||||
// OrderedRefs 拍平的有序列表,Index 供提示词引用
|
||||
OrderedRefs []*OrderedRef
|
||||
}
|
||||
|
||||
// BuildGenerationContext 构建一次生成会话的上下文
|
||||
func BuildGenerationContext(ctx context.Context, drama *entity.Drama) (*GenerationContext, error) {
|
||||
characters, _, err := dao.Character.ListPageByDrama(ctx, drama.Id, 1, -1)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("加载演员失败: %w", err)
|
||||
}
|
||||
|
||||
scenes, err := dao.Scene.ListByDrama(ctx, drama.Id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("加载场景失败: %w", err)
|
||||
}
|
||||
|
||||
props, err := dao.Prop.ListByDrama(ctx, drama.Id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("加载道具失败: %w", err)
|
||||
}
|
||||
|
||||
ctx2 := &GenerationContext{
|
||||
Drama: drama,
|
||||
Characters: characters,
|
||||
Scenes: scenes,
|
||||
Props: props,
|
||||
RefIndex: make(RefIndex),
|
||||
}
|
||||
|
||||
// 构建 RefIndex
|
||||
if err := ctx2.buildRefIndex(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ctx2, nil
|
||||
}
|
||||
|
||||
// buildRefIndex 根据预加载的实体数据构建 RefIndex 和 OrderedRefs
|
||||
func (c *GenerationContext) buildRefIndex() error {
|
||||
c.RefIndex = make(RefIndex)
|
||||
c.OrderedRefs = nil
|
||||
|
||||
// 演员索引
|
||||
charIdx := make(map[string]string, len(c.Characters))
|
||||
for _, ch := range c.Characters {
|
||||
if ch.PortraitPath == "" {
|
||||
continue
|
||||
}
|
||||
b64, err := imageFileToBase64(ch.PortraitPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if existing, ok := charIdx[ch.Name]; ok {
|
||||
return fmt.Errorf("演员名冲突: '%s' (已有形象路径 %s,重复 %s)", ch.Name, existing, ch.PortraitPath)
|
||||
}
|
||||
charIdx[ch.Name] = b64
|
||||
}
|
||||
if len(charIdx) > 0 {
|
||||
c.RefIndex["演员"] = charIdx
|
||||
}
|
||||
|
||||
// 场景索引
|
||||
sceneIdx := make(map[string]string, len(c.Scenes))
|
||||
for _, sc := range c.Scenes {
|
||||
if sc.ImagePath == "" {
|
||||
continue
|
||||
}
|
||||
b64, err := imageFileToBase64(sc.ImagePath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if _, ok := sceneIdx[sc.Name]; ok {
|
||||
return fmt.Errorf("场景名冲突: '%s'", sc.Name)
|
||||
}
|
||||
sceneIdx[sc.Name] = b64
|
||||
}
|
||||
if len(sceneIdx) > 0 {
|
||||
c.RefIndex["场景"] = sceneIdx
|
||||
}
|
||||
|
||||
// 道具索引
|
||||
propIdx := make(map[string]string, len(c.Props))
|
||||
for _, p := range c.Props {
|
||||
if p.ImagePath == "" {
|
||||
continue
|
||||
}
|
||||
b64, err := imageFileToBase64(p.ImagePath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if _, ok := propIdx[p.Name]; ok {
|
||||
return fmt.Errorf("道具名冲突: '%s'", p.Name)
|
||||
}
|
||||
propIdx[p.Name] = b64
|
||||
}
|
||||
if len(propIdx) > 0 {
|
||||
c.RefIndex["道具"] = propIdx
|
||||
}
|
||||
|
||||
// 构建有序索引(拍平成 OrderedRefs,按 演员→场景→道具 顺序)
|
||||
idx := 0
|
||||
for _, category := range []string{"演员", "场景", "道具"} {
|
||||
m, ok := c.RefIndex[category]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
// 将 map 转为有序列表(保证每次顺序一致)
|
||||
names := make([]string, 0, len(m))
|
||||
for name := range m {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
for _, name := range names {
|
||||
c.OrderedRefs = append(c.OrderedRefs, &OrderedRef{
|
||||
Category: category,
|
||||
Name: name,
|
||||
URL: m[name],
|
||||
Index: idx,
|
||||
})
|
||||
idx++
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LookupRef 按实体类型和名称查找引用 URL,返回空字符串表示未找到
|
||||
func (c *GenerationContext) LookupRef(category, name string) string {
|
||||
if m, ok := c.RefIndex[category]; ok {
|
||||
return m[name]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// FindRefIndex 按实体类型和名称查找其在 OrderedRefs 中的索引位置,返回 -1 表示未找到
|
||||
func (c *GenerationContext) FindRefIndex(category, name string) int {
|
||||
for _, ref := range c.OrderedRefs {
|
||||
if ref.Category == category && ref.Name == name {
|
||||
return ref.Index
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
|
||||
const workspaceRoot = "workspace"
|
||||
|
||||
var workspaceSubdirs = []string{"产出视频", "演员形象", "演员声音", "场景", "道具", "剧集描述", "脚本"}
|
||||
var workspaceSubdirs = []string{"产出视频", "演员形象", "演员声音", "场景", "道具"}
|
||||
|
||||
// sanitizeDirName 将短剧标题转为安全的目录名
|
||||
func sanitizeDirName(title string) string {
|
||||
|
||||
Reference in New Issue
Block a user