// Package media 统一生成可长期稳定引用的 COS 媒体对象键和扩展名。 package media import ( "fmt" "path/filepath" "regexp" "strings" "time" "github.com/google/uuid" ) const objectRoot = "juyou_ran" var unsafeKeyPart = regexp.MustCompile(`[^a-z0-9-]+`) var safeExtension = regexp.MustCompile(`^\.[a-z0-9]+$`) // objectKey 为所有媒体对象添加统一的业务根目录,避免不同业务直接污染存储桶根目录。 func objectKey(format string, args ...any) string { return fmt.Sprintf(objectRoot+"/"+format, args...) } // Slug 将业务名称转换为受控英文路径片段;展示名称保留在数据库中。 func Slug(value, fallback string) string { value = strings.ToLower(strings.TrimSpace(value)) value = unsafeKeyPart.ReplaceAllString(value, "-") value = strings.Trim(value, "-") if value == "" { value = fallback } if len(value) > 48 { value = strings.Trim(value[:48], "-") } return value } // Extension 根据媒体类型或安全的原文件扩展名生成对象扩展名。 func Extension(filename, contentType string) string { switch strings.ToLower(contentType) { case "image/jpeg": return ".jpg" case "image/png": return ".png" case "image/webp": return ".webp" case "image/gif": return ".gif" case "video/mp4": return ".mp4" case "video/webm": return ".webm" case "audio/mpeg": return ".mp3" case "audio/wav", "audio/x-wav": return ".wav" case "text/vtt": return ".vtt" } ext := strings.ToLower(filepath.Ext(filename)) if len(ext) > 1 && len(ext) <= 8 && safeExtension.MatchString(ext) { return ext } return ".bin" } // EpisodeSource 返回剧集原视频的对象键。 func EpisodeSource(projectID uuid.UUID, episodeNo int, episodeID, assetID uuid.UUID, filename, contentType string) string { return objectKey("video-redraw/projects/%s/episodes/%03d-%s/source/%s-source%s", projectID, episodeNo, episodeID, assetID, Extension(filename, contentType)) } // EpisodeSubtitle 返回剧集字幕的对象键。 func EpisodeSubtitle(projectID uuid.UUID, episodeNo int, episodeID, assetID uuid.UUID, filename, contentType string) string { return objectKey("video-redraw/projects/%s/episodes/%03d-%s/subtitles/%s-subtitle%s", projectID, episodeNo, episodeID, assetID, Extension(filename, contentType)) } // ProjectSource 返回无剧集项目原视频的对象键。 func ProjectSource(projectID, assetID uuid.UUID, filename, contentType string) string { return objectKey("video-redraw/projects/%s/source/%s-source%s", projectID, assetID, Extension(filename, contentType)) } // ProjectSubtitle 返回无剧集项目字幕的对象键。 func ProjectSubtitle(projectID, assetID uuid.UUID, filename, contentType string) string { return objectKey("video-redraw/projects/%s/subtitles/%s-subtitle%s", projectID, assetID, Extension(filename, contentType)) } // AssetReference 返回项目资产参考媒体的对象键。 func AssetReference(projectID uuid.UUID, assetType string, assetID, mediaID uuid.UUID, filename, contentType string) string { return objectKey("video-redraw/projects/%s/assets/%s/%s/%s-reference%s", projectID, Slug(assetType, "custom"), assetID, mediaID, Extension(filename, contentType)) } // ProductImageReference 返回独立图片生成工具参考图片的稳定对象键。 func ProductImageReference(userID, mediaID uuid.UUID, filename, contentType string) string { return objectKey("product-images/users/%s/references/%s%s", userID, mediaID, Extension(filename, contentType)) } // AssetOutput 返回项目资产生成结果的对象键。 func AssetOutput(projectID uuid.UUID, assetType string, assetID, taskID, mediaID uuid.UUID, contentType string) string { return objectKey("video-redraw/projects/%s/assets/%s/%s/outputs/%s-%s-result%s", projectID, Slug(assetType, "custom"), assetID, taskID, mediaID, Extension("", contentTypeFor("image", contentType))) } // StoryboardFrame 返回剧集分镜抽帧的对象键。 func StoryboardFrame(projectID, episodeID, storyboardID, mediaID uuid.UUID, sequence int, timestampMS int64) string { return objectKey("video-redraw/projects/%s/episodes/%s/storyboards/%04d-%s/frames/%s-%010d.jpg", projectID, episodeID, sequence, storyboardID, mediaID, timestampMS) } // StoryboardAnalysis 返回剧集分镜反推原始响应的对象键。 func StoryboardAnalysis(projectID, episodeID, storyboardID, taskID, mediaID uuid.UUID, sequence int) string { return objectKey("video-redraw/projects/%s/episodes/%s/storyboards/%04d-%s/analysis/%s-%s-reverse.json", projectID, episodeID, sequence, storyboardID, taskID, mediaID) } // TaskOutput 返回剧集分镜生成结果的对象键。 func TaskOutput(projectID, episodeID uuid.UUID, sequence int, taskID, mediaID uuid.UUID, outputType, contentType string) string { return objectKey("video-redraw/projects/%s/episodes/%s/storyboards/%04d/outputs/%s-%s-result%s", projectID, episodeID, sequence, taskID, mediaID, Extension("", contentTypeFor(outputType, contentType))) } // ProjectStoryboardFrame 返回无剧集项目分镜抽帧的对象键。 func ProjectStoryboardFrame(projectID, storyboardID, mediaID uuid.UUID, sequence int, timestampMS int64) string { return objectKey("video-redraw/projects/%s/storyboards/%04d-%s/frames/%s-%010d.jpg", projectID, sequence, storyboardID, mediaID, timestampMS) } // ProjectStoryboardAnalysis 返回无剧集项目分镜反推原始响应的对象键。 func ProjectStoryboardAnalysis(projectID, storyboardID, taskID, mediaID uuid.UUID, sequence int) string { return objectKey("video-redraw/projects/%s/storyboards/%04d-%s/analysis/%s-%s-reverse.json", projectID, sequence, storyboardID, taskID, mediaID) } // ProjectTaskOutput 返回无剧集项目分镜生成结果的对象键。 func ProjectTaskOutput(projectID uuid.UUID, sequence int, taskID, mediaID uuid.UUID, outputType, contentType string) string { return objectKey("video-redraw/projects/%s/storyboards/%04d/outputs/%s-%s-result%s", projectID, sequence, taskID, mediaID, Extension("", contentTypeFor(outputType, contentType))) } // StyleImage 返回管理端风格图片的统一对象键。 func StyleImage(mediaID uuid.UUID, extension string) string { return objectKey("styles/%s/%s%s", time.Now().Format("2006/01"), mediaID, extension) } // UserAvatar 返回用户头像的统一对象键。 func UserAvatar(userID, mediaID uuid.UUID, extension string) string { return objectKey("users/%s/avatar/%s%s", userID, mediaID, extension) } // StandaloneTaskOutput 返回独立生成任务的结果对象键。 func StandaloneTaskOutput(userID, taskID uuid.UUID, outputType, contentType string) string { return objectKey("users/%s/tasks/%s/output-1%s", userID, taskID, Extension("", contentTypeFor(outputType, contentType))) } // contentTypeFor 返回已知内容类型,缺省时根据输出类型选择默认媒体类型。 func contentTypeFor(outputType, contentType string) string { if contentType != "" { return contentType } if outputType == "image" { return "image/png" } return "video/mp4" }