diff --git a/core/backend/apps/assets/tests.py b/core/backend/apps/assets/tests.py index 9c8e779..baf1f67 100644 --- a/core/backend/apps/assets/tests.py +++ b/core/backend/apps/assets/tests.py @@ -93,8 +93,8 @@ class AssetSoftDeleteTests(TestCase): self.user, self.team = _mk_team("usd", "TeamSD") self.client = APIClient() self.client.force_authenticate(self.user) - self.live = Asset.objects.create(team=self.team, name="留存图", asset_type="image", source="ai_generated", category=Asset.Category.PERSON) - self.dead = Asset.objects.create(team=self.team, name="废图", asset_type="image", source="ai_generated", category=Asset.Category.PERSON, is_deleted=True) + self.live = Asset.objects.create(team=self.team, name="留存图", asset_type="image", source="ai_generated", category=Asset.Category.MODEL_TRYON) + self.dead = Asset.objects.create(team=self.team, name="废图", asset_type="image", source="ai_generated", category=Asset.Category.MODEL_TRYON, is_deleted=True) def test_list_excludes_soft_deleted(self): ids = {a["id"] for a in self.client.get("/api/assets/?page_size=50").json()["results"]} @@ -102,7 +102,43 @@ class AssetSoftDeleteTests(TestCase): self.assertNotIn(str(self.dead.id), ids) def test_summary_excludes_soft_deleted(self): - self.assertEqual(self.client.get("/api/assets/summary/").json()["people"], 1) + self.assertEqual(self.client.get("/api/assets/summary/").json()["tryon"], 1) + + +class VideoPacksTests(TestCase): + """视频成品按项目素材包分组(与导出同源:取 VideoSegment.adopted_version 的真实视频资产)。""" + + def setUp(self): + from apps.products.models import Product + from apps.projects.models import Project, VideoSegment, VideoSegmentVersion + from .models import AssetFile + + self.user, self.team = _mk_team("uvp", "TeamVP") + self.client = APIClient() + self.client.force_authenticate(self.user) + self.product = Product.objects.create(team=self.team, created_by=self.user, title="P") + self.proj = Project.objects.create(team=self.team, name="项目甲", product=self.product, created_by=self.user) + self.assets = [] + for i in range(2): + a = Asset.objects.create(team=self.team, name=f"clip{i}", asset_type="video", source="ai_generated", category=Asset.Category.VIDEO_CLIP) + AssetFile.objects.create(asset=a, object_key=f"k{i}.mp4", bucket="b", content_type="video/mp4", is_primary=True) + seg = VideoSegment.objects.create(project=self.proj, sort_order=i) + ver = VideoSegmentVersion.objects.create(video_segment=seg, asset=a, is_adopted=True) + seg.adopted_version = ver + seg.save(update_fields=["adopted_version"]) + self.assets.append(a) + + def test_packs_grouped_by_project(self): + data = self.client.get("/api/assets/video-packs/").json() + self.assertEqual(len(data), 1) + self.assertEqual(data[0]["project_name"], "项目甲") + self.assertEqual(len(data[0]["clips"]), 2) + self.assertIn("url", data[0]["clips"][0]) + + def test_soft_deleted_clip_excluded(self): + Asset.objects.filter(id=self.assets[0].id).update(is_deleted=True) + data = self.client.get("/api/assets/video-packs/").json() + self.assertEqual(len(data[0]["clips"]), 1) class ReviewScopeTests(TestCase): diff --git a/core/backend/apps/assets/views.py b/core/backend/apps/assets/views.py index ca44fe8..2346b2f 100644 --- a/core/backend/apps/assets/views.py +++ b/core/backend/apps/assets/views.py @@ -54,6 +54,8 @@ def _tab_q(tab: str) -> Q: return Q(category="upload") if tab == "materials": # 素材(期3):视频素材 = 所有视频,排除「最终成片」(final_video 隐藏不列) return ~Q(category="final_video") & Q(asset_type="video") + if tab == "others": # 其他(资产库成品化):我的上传 + 未归类非视频(兜底) + return Q(category="upload") | (~Q(category__in=_KNOWN_CATS) & ~Q(asset_type="video")) if tab == "unclassified": # 未归类且非视频(也不含最终成片) return ~Q(category__in=_KNOWN_CATS) & ~Q(asset_type="video") return Q() @@ -110,9 +112,50 @@ class AssetViewSet(TeamScopedViewSetMixin, ModelViewSet): def summary(self, request): """资产库 tab 计数(人物/场景/商品图/成片/我的上传/未分类),供 tab 徽标——不必取全量。""" base = Asset.objects.filter(team=self.get_team(), is_deleted=False) - tabs = ["people", "scenes", "products", "tryon", "kits", "creations", "materials", "uploads", "unclassified"] + # 资产库成品化:对外只数成品三类 + 其他(角色/场景/商品图各回各库,视频成品走 video-packs) + tabs = ["tryon", "kits", "creations", "others"] return Response({t: base.filter(_tab_q(t)).count() for t in tabs}) + @action(detail=False, methods=["get"], url_path="video-packs") + def video_packs(self, request): + """视频成品按「项目素材包」打包:每个项目的已采用视频片段 = 一个包(与「导出全部」同源: + 取 VideoSegment.adopted_version 的真实视频资产)。片段给 TOS 直链 url 供