初始化

This commit is contained in:
Ran
2026-08-25 17:59:42 +08:00
commit 4b7380dd9b
408 changed files with 327400 additions and 0 deletions
+156
View File
@@ -0,0 +1,156 @@
// 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"
}
+17
View File
@@ -0,0 +1,17 @@
package media
import (
"strings"
"testing"
"github.com/google/uuid"
)
func TestAssetKeysUseControlledSegments(t *testing.T) {
projectID, assetID, taskID, mediaID := uuid.New(), uuid.New(), uuid.New(), uuid.New()
key := AssetOutput(projectID, "角色 中文", assetID, taskID, mediaID, "image/webp")
wantPrefix := "juyou_ran/video-redraw/projects/" + projectID.String() + "/assets/custom/" + assetID.String() + "/outputs/"
if !strings.HasPrefix(key, wantPrefix) || !strings.HasSuffix(key, ".webp") {
t.Fatalf("unexpected key: %s", key)
}
}