* feat(editor): 剪辑成片验收闭环 - 概览渲染成功计数、媒体导入/字幕/预览修复与本地 whisper 转写运行时 - 概览:渲染成功计数接入项目概览 SQL/JSON,成片状态随 timeline_render 任务联动 - 修复:本地媒体导入后列表为空(挂载时服务端重同步 + 导入后校验重取)、 字幕工具残留 M6 占位文案、预览监视器悬浮遮挡层改为顶部信息条、 生成任务轮询连续查询失败即抛错不再空转至超时 - 本地转写:新增 scripts/start-whisper-local.sh 一键启动 whisper.cpp, local-development 文档补安装/启动/后端注入指引 - 同步 docs(overview/features、pending-test、index) * fix(assets): 素材库封面未知 kind 兜底,避免 AssetCover 渲染崩溃 运行时素材记录若带未知/脏 kind(历史遗留或本地存储异常),assetKindIcons[asset.kind] 得到 undefined,AssetCover/AssetDrawer 渲染 <KindIcon/> 触发 'Element type is invalid ... got: undefined' 使 /assets 整页崩溃。 新增 isKnownAssetKind 守卫:未知 kind 回退 FileText 图标与通用文案,脏数据不再崩页。 * fix(editor): mp4v 播放死循环修复——MPEG-4 Part 2 归入播放转码通道,监视器加终态护栏 - 后端把 mp4v 与 H.264/AV1/VP9 一并判为浏览器可播 → playbackStatus=none; Chromium/Firefox/Edge 实际均不支持 MPEG-4 Part 2,原件解码即失败。 现将其并入 H.265 转码通道(转 H.264 兼容副本);损坏文件转码失败落 failed,前端据此显示终止性错误而非死循环。 - 前端监视器轮询到 none/failed 或超时后不再回退 primary 重试;副本 URL 回退原件反复触发 onError 时保持终态文案,不再覆盖成"正在生成兼容版本"。 * feat(editor): 画布产物自动同步剪辑素材库,素材面板新增「画布素材」分组 - 打开剪辑器时自动把画布工作台当前采用(storyboard/action_board/分镜/导出成片, ready 且有资源)的产物 linkProjectAsset(source=canvas) 并入项目素材库; 后端幂等、前端按 storageKey+会话 ref 去重,失败静默下次进入自动补同步。 - LinkProjectAsset 支持 source 白名单(uploaded/canvas)并写入素材 payload, 供素材面板按来源分组;面板新增「画布素材」页签与分组,与「本地上传」「项目素材」并列。 - 补 source=canvas 写入断言测试。 * docs(progress): 记录 mp4v 播放死循环修复与画布产物素材同步的待浏览器验收项
112 lines
3.3 KiB
Go
112 lines
3.3 KiB
Go
package service
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
|
|
"infinite-canvas/backend/internal/model"
|
|
"infinite-canvas/backend/internal/repository"
|
|
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
func newProjectAssetLinkTestService(t *testing.T) (*Service, *gorm.DB) {
|
|
t.Helper()
|
|
db, err := gorm.Open(sqlite.Open("file:"+newID()+"?mode=memory&cache=shared"), &gorm.Config{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.AutoMigrate(
|
|
&model.Project{},
|
|
&model.ProjectAssetLink{},
|
|
&model.ProjectAssetFolder{},
|
|
&model.Asset{},
|
|
&model.AssetVersion{},
|
|
&model.AssetRepresentation{},
|
|
&model.Resource{},
|
|
&model.Shot{},
|
|
&model.ShotAssetReference{},
|
|
); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return &Service{repo: repository.New(db)}, db
|
|
}
|
|
|
|
// TestLinkProjectAssetCreatesAssetFromUploadedResource 覆盖媒体导入场景:
|
|
// 上传只落 resources 表,首次链接时服务端应按资源元数据合成资产记录,
|
|
// 否则“资源存在但无资产记录”会让导入永远失败。
|
|
func TestLinkProjectAssetCreatesAssetFromUploadedResource(t *testing.T) {
|
|
service, db := newProjectAssetLinkTestService(t)
|
|
project := model.Project{ID: "project-1", UserID: "user-1", Name: "短剧", Status: model.ProjectStatusActive}
|
|
resource := model.Resource{
|
|
ID: "resource-1",
|
|
UserID: "user-1",
|
|
Kind: "video",
|
|
Status: model.ResourceStatusReady,
|
|
MimeType: "video/mp4",
|
|
Size: 1024,
|
|
Width: 1920,
|
|
Height: 1080,
|
|
}
|
|
for _, item := range []any{&project, &resource} {
|
|
if err := db.Create(item).Error; err != nil {
|
|
t.Fatalf("seed %T: %v", item, err)
|
|
}
|
|
}
|
|
|
|
summary, err := service.LinkProjectAsset("user-1", "project-1", LinkProjectAssetRequest{
|
|
AssetID: "resource-1",
|
|
Category: "",
|
|
Title: "开场.mp4",
|
|
Source: AssetSourceCanvas,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("LinkProjectAsset: %v", err)
|
|
}
|
|
if summary.ID != "resource-1" {
|
|
t.Fatalf("summary.ID = %q, want resource-1", summary.ID)
|
|
}
|
|
|
|
var asset model.Asset
|
|
if err := db.First(&asset, "id = ?", "resource-1").Error; err != nil {
|
|
t.Fatalf("asset not created: %v", err)
|
|
}
|
|
if asset.Title != "开场.mp4" {
|
|
t.Errorf("asset.Title = %q, want 开场.mp4", asset.Title)
|
|
}
|
|
if asset.Category != model.AssetCategoryMaterial {
|
|
t.Errorf("asset.Category = %q, want material", asset.Category)
|
|
}
|
|
if summary.Source != AssetSourceCanvas {
|
|
t.Errorf("summary.Source = %q, want canvas", summary.Source)
|
|
}
|
|
var payload struct {
|
|
Data struct {
|
|
Source string `json:"source"`
|
|
} `json:"data"`
|
|
}
|
|
if err := json.Unmarshal([]byte(asset.PayloadJSON), &payload); err != nil {
|
|
t.Fatalf("payload unmarshal: %v", err)
|
|
}
|
|
if payload.Data.Source != AssetSourceCanvas {
|
|
t.Errorf("payload.source = %q, want canvas", payload.Data.Source)
|
|
}
|
|
}
|
|
|
|
// TestLinkProjectAssetMissingResourceKeepsFailingClosed 资源与资产都不存在时仍应报错,
|
|
// 不允许凭空链接。
|
|
func TestLinkProjectAssetMissingResourceKeepsFailingClosed(t *testing.T) {
|
|
service, db := newProjectAssetLinkTestService(t)
|
|
project := model.Project{ID: "project-2", UserID: "user-2", Name: "短剧", Status: model.ProjectStatusActive}
|
|
if err := db.Create(&project).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if _, err := service.LinkProjectAsset("user-2", "project-2", LinkProjectAssetRequest{
|
|
AssetID: "no-such-resource",
|
|
}); err == nil {
|
|
t.Fatal("LinkProjectAsset succeeded for missing resource, want error")
|
|
}
|
|
}
|