package services import ( "encoding/json" "fmt" models "github.com/drama-generator/backend/domain/models" "github.com/drama-generator/backend/pkg/logger" "gorm.io/gorm" ) type StoryboardCompositionService struct { db *gorm.DB log *logger.Logger imageGen *ImageGenerationService } func NewStoryboardCompositionService(db *gorm.DB, log *logger.Logger, imageGen *ImageGenerationService) *StoryboardCompositionService { return &StoryboardCompositionService{ db: db, log: log, imageGen: imageGen, } } type SceneCharacterInfo struct { ID uint `json:"id"` Name string `json:"name"` ImageURL *string `json:"image_url,omitempty"` } type SceneBackgroundInfo struct { ID uint `json:"id"` Location string `json:"location"` Time string `json:"time"` ImageURL *string `json:"image_url,omitempty"` Status string `json:"status"` } type SceneCompositionInfo struct { ID uint `json:"id"` StoryboardNumber int `json:"storyboard_number"` Title *string `json:"title"` Description *string `json:"description"` Location *string `json:"location"` Time *string `json:"time"` Duration int `json:"duration"` Dialogue *string `json:"dialogue"` Action *string `json:"action"` Atmosphere *string `json:"atmosphere"` ImagePrompt *string `json:"image_prompt,omitempty"` VideoPrompt *string `json:"video_prompt,omitempty"` Characters []SceneCharacterInfo `json:"characters"` Background *SceneBackgroundInfo `json:"background"` SceneID *uint `json:"scene_id"` ComposedImage *string `json:"composed_image,omitempty"` VideoURL *string `json:"video_url,omitempty"` ImageGenerationID *uint `json:"image_generation_id,omitempty"` ImageGenerationStatus *string `json:"image_generation_status,omitempty"` VideoGenerationID *uint `json:"video_generation_id,omitempty"` VideoGenerationStatus *string `json:"video_generation_status,omitempty"` } func (s *StoryboardCompositionService) GetScenesForEpisode(episodeID string) ([]SceneCompositionInfo, error) { // 验证权限 var episode models.Episode err := s.db.Preload("Drama").Where("id = ?", episodeID).First(&episode).Error if err != nil { s.log.Errorw("Episode not found", "episode_id", episodeID, "error", err) return nil, fmt.Errorf("episode not found") } s.log.Infow("GetScenesForEpisode auth check", "episode_id", episodeID, "drama_id", episode.DramaID) // 获取分镜列表 var storyboards []models.Storyboard if err := s.db.Where("episode_id = ?", episodeID). Preload("Characters"). Order("storyboard_number ASC"). Find(&storyboards).Error; err != nil { return nil, fmt.Errorf("failed to load storyboards: %w", err) } // 获取所有角色(用于匹配角色信息) var characters []models.Character if err := s.db.Where("drama_id = ?", episode.DramaID).Find(&characters).Error; err != nil { s.log.Warnw("Failed to load characters", "error", err) } // 创建角色ID到角色信息的映射 charIDToInfo := make(map[uint]*models.Character) for i := range characters { charIDToInfo[characters[i].ID] = &characters[i] } // 获取所有场景ID var sceneIDs []uint for _, storyboard := range storyboards { if storyboard.SceneID != nil { sceneIDs = append(sceneIDs, *storyboard.SceneID) } } // 批量获取场景信息 var scenes []models.Scene sceneMap := make(map[uint]*models.Scene) if len(sceneIDs) > 0 { if err := s.db.Where("id IN ?", sceneIDs).Find(&scenes).Error; err == nil { for i := range scenes { sceneMap[scenes[i].ID] = &scenes[i] } } } // 获取分镜的合成图片(从 image_generations 表) storyboardIDs := make([]uint, len(storyboards)) for i, storyboard := range storyboards { storyboardIDs[i] = storyboard.ID } imageGenMap := make(map[uint]string) // storyboard_id -> image_url imageGenTaskMap := make(map[uint]*models.ImageGeneration) // storyboard_id -> processing task if len(storyboardIDs) > 0 { var imageGens []models.ImageGeneration // 查询已完成的图片生成记录,每个镜头只取最新的一条 if err := s.db.Where("storyboard_id IN ? AND status = ?", storyboardIDs, models.ImageStatusCompleted). Order("created_at DESC"). Find(&imageGens).Error; err == nil { // 为每个镜头保留最新的一条记录 for _, ig := range imageGens { if ig.StoryboardID != nil { if _, exists := imageGenMap[*ig.StoryboardID]; !exists { if ig.ImageURL != nil { imageGenMap[*ig.StoryboardID] = *ig.ImageURL } } } } } // 查询进行中的图片生成任务 var processingImageGens []models.ImageGeneration if err := s.db.Where("storyboard_id IN ? AND status = ?", storyboardIDs, models.ImageStatusProcessing). Order("created_at DESC"). Find(&processingImageGens).Error; err == nil { for _, ig := range processingImageGens { if ig.StoryboardID != nil { if _, exists := imageGenTaskMap[*ig.StoryboardID]; !exists { igCopy := ig imageGenTaskMap[*ig.StoryboardID] = &igCopy } } } } } // 批量查询进行中的视频生成任务 videoGenTaskMap := make(map[uint]*models.VideoGeneration) // storyboard_id -> processing task if len(storyboardIDs) > 0 { var processingVideoGens []models.VideoGeneration if err := s.db.Where("scene_id IN ? AND status = ?", storyboardIDs, models.VideoStatusProcessing). Order("created_at DESC"). Find(&processingVideoGens).Error; err == nil { for _, vg := range processingVideoGens { if vg.StoryboardID != nil { if _, exists := videoGenTaskMap[*vg.StoryboardID]; !exists { vgCopy := vg videoGenTaskMap[*vg.StoryboardID] = &vgCopy } } } } } // 构建返回结果 var result []SceneCompositionInfo for _, storyboard := range storyboards { storyboardInfo := SceneCompositionInfo{ ID: storyboard.ID, StoryboardNumber: storyboard.StoryboardNumber, Title: storyboard.Title, Description: storyboard.Description, Location: storyboard.Location, Time: storyboard.Time, Duration: storyboard.Duration, Action: storyboard.Action, Dialogue: storyboard.Dialogue, Atmosphere: storyboard.Atmosphere, ImagePrompt: storyboard.ImagePrompt, VideoPrompt: storyboard.VideoPrompt, SceneID: storyboard.SceneID, } // 直接使用关联的角色信息 if len(storyboard.Characters) > 0 { for _, char := range storyboard.Characters { storyboardChar := SceneCharacterInfo{ ID: char.ID, Name: char.Name, ImageURL: char.ImageURL, } storyboardInfo.Characters = append(storyboardInfo.Characters, storyboardChar) } } // 添加场景信息 if storyboard.SceneID != nil { if scene, ok := sceneMap[*storyboard.SceneID]; ok { storyboardInfo.Background = &SceneBackgroundInfo{ ID: scene.ID, Location: scene.Location, Time: scene.Time, ImageURL: scene.ImageURL, Status: scene.Status, } } } // 添加合成图片 if imageURL, ok := imageGenMap[storyboard.ID]; ok { storyboardInfo.ComposedImage = &imageURL } // 添加视频URL if storyboard.VideoURL != nil { storyboardInfo.VideoURL = storyboard.VideoURL } // 添加进行中的图片生成任务信息 if imageTask, ok := imageGenTaskMap[storyboard.ID]; ok { storyboardInfo.ImageGenerationID = &imageTask.ID statusStr := string(imageTask.Status) storyboardInfo.ImageGenerationStatus = &statusStr } // 添加进行中的视频生成任务信息 if videoTask, ok := videoGenTaskMap[storyboard.ID]; ok { storyboardInfo.VideoGenerationID = &videoTask.ID statusStr := string(videoTask.Status) storyboardInfo.VideoGenerationStatus = &statusStr } result = append(result, storyboardInfo) } return result, nil } type UpdateSceneRequest struct { SceneID *uint `json:"scene_id"` Characters []uint `json:"characters"` // 改为存储角色ID数组 Location *string `json:"location"` Time *string `json:"time"` Action *string `json:"action"` Dialogue *string `json:"dialogue"` Description *string `json:"description"` Duration *int `json:"duration"` ImagePrompt *string `json:"image_prompt"` VideoPrompt *string `json:"video_prompt"` } func (s *StoryboardCompositionService) UpdateScene(sceneID string, req *UpdateSceneRequest) error { // 获取分镜并验证权限 var storyboard models.Storyboard err := s.db.Preload("Episode.Drama").Where("id = ?", sceneID).First(&storyboard).Error if err != nil { return fmt.Errorf("scene not found") } // 构建更新数据 updates := make(map[string]interface{}) // 更新背景ID if req.SceneID != nil { updates["scene_id"] = req.SceneID } // 更新角色列表(直接存储ID数组) if req.Characters != nil { charactersJSON, err := json.Marshal(req.Characters) if err != nil { return fmt.Errorf("failed to serialize characters: %w", err) } updates["characters"] = charactersJSON } // 更新场景信息字段 if req.Location != nil { updates["location"] = req.Location } if req.Time != nil { updates["time"] = req.Time } if req.Action != nil { updates["action"] = req.Action } if req.Dialogue != nil { updates["dialogue"] = req.Dialogue } if req.Description != nil { updates["description"] = req.Description } if req.Duration != nil { updates["duration"] = *req.Duration } if req.ImagePrompt != nil { updates["image_prompt"] = req.ImagePrompt } if req.VideoPrompt != nil { updates["video_prompt"] = req.VideoPrompt } // 执行更新 if len(updates) > 0 { if err := s.db.Model(&models.Storyboard{}).Where("id = ?", sceneID).Updates(updates).Error; err != nil { return fmt.Errorf("failed to update scene: %w", err) } } s.log.Infow("Scene updated", "scene_id", sceneID, "updates", updates) return nil } type GenerateSceneImageRequest struct { SceneID uint `json:"scene_id"` Prompt string `json:"prompt"` Model string `json:"model"` } func (s *StoryboardCompositionService) GenerateSceneImage(req *GenerateSceneImageRequest) (*models.ImageGeneration, error) { // 获取场景并验证权限 var scene models.Scene err := s.db.Where("id = ?", req.SceneID).First(&scene).Error if err != nil { return nil, fmt.Errorf("scene not found") } // 验证权限:通过DramaID查询Drama var drama models.Drama if err := s.db.Where("id = ? ", scene.DramaID).First(&drama).Error; err != nil { return nil, fmt.Errorf("unauthorized") } // 构建场景图片生成提示词 prompt := req.Prompt if prompt == "" { // 使用场景的Prompt字段 prompt = scene.Prompt if prompt == "" { // 如果Prompt为空,使用Location和Time构建 prompt = fmt.Sprintf("%s场景,%s", scene.Location, scene.Time) } s.log.Infow("Using scene prompt", "scene_id", req.SceneID, "prompt", prompt) } // 使用imageGen服务直接生成 if s.imageGen != nil { genReq := &GenerateImageRequest{ SceneID: &req.SceneID, DramaID: fmt.Sprintf("%d", scene.DramaID), ImageType: string(models.ImageTypeScene), Prompt: prompt, Model: req.Model, // 使用用户指定的模型 Size: "2560x1440", // 3,686,400像素,满足doubao模型最低要求(16:9比例) Quality: "standard", } imageGen, err := s.imageGen.GenerateImage(genReq) if err != nil { return nil, fmt.Errorf("failed to generate image: %w", err) } // 更新场景的image_url if imageGen.ImageURL != nil { scene.ImageURL = imageGen.ImageURL scene.Status = "generated" if err := s.db.Save(&scene).Error; err != nil { s.log.Errorw("Failed to update scene image url", "error", err) } } s.log.Infow("Scene image generation created", "scene_id", req.SceneID, "image_gen_id", imageGen.ID) return imageGen, nil } return nil, fmt.Errorf("image generation service not available") } func getStringValue(s *string) string { if s != nil { return *s } return "" }