diff --git a/api/handlers/asset.go b/api/handlers/asset.go index ebf6585..a41387e 100644 --- a/api/handlers/asset.go +++ b/api/handlers/asset.go @@ -218,27 +218,3 @@ func (h *AssetHandler) ImportFromVideoGen(c *gin.Context) { response.Success(c, asset) } - -// ExtractFrame 从视频素材中提取帧 -func (h *AssetHandler) ExtractFrame(c *gin.Context) { - assetID, err := strconv.ParseUint(c.Param("id"), 10, 32) - if err != nil { - response.BadRequest(c, "无效的ID") - return - } - - var req services.ExtractFrameRequest - if err := c.ShouldBindJSON(&req); err != nil { - response.BadRequest(c, err.Error()) - return - } - - imageGen, err := h.assetService.ExtractFrameFromAsset(uint(assetID), &req) - if err != nil { - h.log.Errorw("Failed to extract frame", "error", err, "asset_id", assetID) - response.InternalError(c, err.Error()) - return - } - - response.Success(c, imageGen) -} diff --git a/api/routes/routes.go b/api/routes/routes.go index 98b9c6d..a97fc92 100644 --- a/api/routes/routes.go +++ b/api/routes/routes.go @@ -181,7 +181,6 @@ func SetupRouter(cfg *config.Config, db *gorm.DB, log *logger.Logger, localStora assets.DELETE("/:id", assetHandler.DeleteAsset) assets.POST("/import/image/:image_gen_id", assetHandler.ImportFromImageGen) assets.POST("/import/video/:video_gen_id", assetHandler.ImportFromVideoGen) - assets.POST("/:id/extract-frame", assetHandler.ExtractFrame) } storyboards := api.Group("/storyboards") diff --git a/application/services/asset_service.go b/application/services/asset_service.go index e479ea3..c07687d 100644 --- a/application/services/asset_service.go +++ b/application/services/asset_service.go @@ -2,11 +2,8 @@ package services import ( "fmt" - "os" - "path/filepath" "strconv" "strings" - "time" models "github.com/drama-generator/backend/domain/models" "github.com/drama-generator/backend/infrastructure/external/ffmpeg" @@ -326,120 +323,3 @@ func (s *AssetService) ImportFromVideoGen(videoGenID uint) (*models.Asset, error return asset, nil } - -// ExtractFrameRequest 视频帧提取请求 -type ExtractFrameRequest struct { - Position string `json:"position"` // "first" 或 "last" - StoryboardID uint `json:"storyboard_id"` // 关联的分镜 ID - FrameType string `json:"frame_type"` // image_generations 的帧类型,默认 "first" -} - -// ExtractFrameFromAsset 从视频素材中提取帧并保存到 image_generations 表 -func (s *AssetService) ExtractFrameFromAsset(assetID uint, req *ExtractFrameRequest) (*models.ImageGeneration, error) { - // 获取素材 - var asset models.Asset - if err := s.db.First(&asset, assetID).Error; err != nil { - return nil, fmt.Errorf("asset not found: %w", err) - } - - // 验证是否为视频素材 - if asset.Type != models.AssetTypeVideo { - return nil, fmt.Errorf("asset is not a video") - } - - // 默认值 - position := req.Position - if position == "" { - position = "last" - } - frameType := req.FrameType - if frameType == "" { - frameType = "first" // 提取的尾帧用作下一个分镜的首帧 - } - - // 生成唯一文件名 - timestamp := time.Now().Format("20060102_150405") - fileName := fmt.Sprintf("frame_%s_%d_%s.jpg", position, assetID, timestamp) - - // 创建临时目录 - tmpDir := "./data/tmp" - if err := os.MkdirAll(tmpDir, 0755); err != nil { - return nil, fmt.Errorf("failed to create temp directory: %w", err) - } - outputPath := filepath.Join(tmpDir, fileName) - - // 使用 FFmpeg 提取帧 - _, err := s.ffmpeg.ExtractFrame(asset.URL, outputPath, position) - if err != nil { - return nil, fmt.Errorf("failed to extract frame: %w", err) - } - - var imageURL string - - // 根据存储类型上传 - if s.ossStorage != nil { - // 上传到 OSS - url, err := s.ossStorage.UploadWithFilename(outputPath, "extracted_frames", fileName) - if err != nil { - s.log.Errorw("Failed to upload to OSS, falling back to local", "error", err) - } else { - imageURL = url - s.log.Infow("Frame uploaded to OSS", "url", url) - // 删除临时文件 - os.Remove(outputPath) - } - } - - // 如果 OSS 上传失败或未配置,使用本地存储 - if imageURL == "" { - localPath := "./data/storage" - baseURL := "/static" - if s.cfg != nil && s.cfg.Storage.LocalPath != "" { - localPath = s.cfg.Storage.LocalPath - } - if s.cfg != nil && s.cfg.Storage.BaseURL != "" { - baseURL = s.cfg.Storage.BaseURL - } - - // 移动文件到最终位置 - finalDir := filepath.Join(localPath, "extracted_frames") - if err := os.MkdirAll(finalDir, 0755); err != nil { - return nil, fmt.Errorf("failed to create output directory: %w", err) - } - finalPath := filepath.Join(finalDir, fileName) - if err := os.Rename(outputPath, finalPath); err != nil { - return nil, fmt.Errorf("failed to move file: %w", err) - } - - imageURL = fmt.Sprintf("%s/extracted_frames/%s", baseURL, fileName) - } - - // 获取 DramaID - var dramaID uint - if asset.DramaID != nil { - dramaID = *asset.DramaID - } - - // 创建 image_generation 记录 - imageGen := &models.ImageGeneration{ - DramaID: dramaID, - StoryboardID: &req.StoryboardID, - Prompt: fmt.Sprintf("Extracted %s frame from video asset #%d", position, assetID), - Status: models.ImageStatusCompleted, - ImageURL: &imageURL, - FrameType: &frameType, - } - - if err := s.db.Create(imageGen).Error; err != nil { - return nil, fmt.Errorf("failed to create image generation record: %w", err) - } - - s.log.Infow("Frame extracted and saved", - "asset_id", assetID, - "position", position, - "image_gen_id", imageGen.ID, - "frame_type", frameType, - "image_url", imageURL) - - return imageGen, nil -} diff --git a/infrastructure/external/ffmpeg/ffmpeg.go b/infrastructure/external/ffmpeg/ffmpeg.go index 4eb6442..9985296 100644 --- a/infrastructure/external/ffmpeg/ffmpeg.go +++ b/infrastructure/external/ffmpeg/ffmpeg.go @@ -853,72 +853,3 @@ func (f *FFmpeg) generateSilence(outputPath string, duration float64) (string, e f.log.Infow("Silence audio generated successfully", "output", outputPath) return outputPath, nil } - -// ExtractFrame 从视频中提取指定位置的帧 -// position: "first" 提取首帧, "last" 提取尾帧 -// 返回提取的图片文件路径 -func (f *FFmpeg) ExtractFrame(videoURL, outputPath, position string) (string, error) { - f.log.Infow("Extracting frame from video", "url", videoURL, "position", position, "output", outputPath) - - // 下载视频文件 - downloadPath := filepath.Join(f.tempDir, fmt.Sprintf("video_%d.mp4", time.Now().Unix())) - localVideoPath, err := f.downloadVideo(videoURL, downloadPath) - if err != nil { - return "", fmt.Errorf("failed to download video: %w", err) - } - defer os.Remove(localVideoPath) - - // 确保输出目录存在 - outputDir := filepath.Dir(outputPath) - if err := os.MkdirAll(outputDir, 0755); err != nil { - return "", fmt.Errorf("failed to create output directory: %w", err) - } - - var cmd *exec.Cmd - - if position == "last" { - // 提取尾帧:先获取视频时长,然后提取最后一帧 - duration, err := f.GetVideoDuration(localVideoPath) - if err != nil { - return "", fmt.Errorf("failed to get video duration: %w", err) - } - - // 提取最后一帧(时长减去0.1秒的位置) - seekTime := duration - 0.1 - if seekTime < 0 { - seekTime = 0 - } - - cmd = exec.Command("ffmpeg", - "-ss", fmt.Sprintf("%.2f", seekTime), - "-i", localVideoPath, - "-vframes", "1", - "-q:v", "2", - "-y", - outputPath, - ) - } else { - // 默认提取首帧 - cmd = exec.Command("ffmpeg", - "-i", localVideoPath, - "-vframes", "1", - "-q:v", "2", - "-y", - outputPath, - ) - } - - output, err := cmd.CombinedOutput() - if err != nil { - f.log.Errorw("FFmpeg frame extraction failed", "error", err, "output", string(output)) - return "", fmt.Errorf("ffmpeg frame extraction failed: %w, output: %s", err, string(output)) - } - - // 检查输出文件是否存在 - if _, err := os.Stat(outputPath); os.IsNotExist(err) { - return "", fmt.Errorf("frame extraction failed: output file not created") - } - - f.log.Infow("Frame extracted successfully", "output", outputPath, "position", position) - return outputPath, nil -} diff --git a/web/src/api/asset.ts b/web/src/api/asset.ts index 6278c15..19c2574 100644 --- a/web/src/api/asset.ts +++ b/web/src/api/asset.ts @@ -43,9 +43,5 @@ export const assetAPI = { importFromVideo(videoGenId: number) { return request.post(`/assets/import/video/${videoGenId}`) - }, - - extractFrame(assetId: number, data: { position: string; storyboard_id: number; frame_type?: string }) { - return request.post(`/assets/${assetId}/extract-frame`, data) } } diff --git a/web/src/views/drama/ProfessionalEditor.vue b/web/src/views/drama/ProfessionalEditor.vue index 0593e4d..f87dc37 100644 --- a/web/src/views/drama/ProfessionalEditor.vue +++ b/web/src/views/drama/ProfessionalEditor.vue @@ -239,24 +239,6 @@ }} - -
- -
- - - - - 提取尾帧 - -
-
-