fix: 自由创作分类入库区分视频与图片展示

This commit is contained in:
hh
2026-07-07 18:39:01 +08:00
parent 45ff7838d8
commit ff723b4281
7 changed files with 228 additions and 22 deletions
+3 -2
View File
@@ -695,7 +695,8 @@ def serialize_free_video_task(task: AITask) -> dict:
return ""
return ""
for asset in task.generated_assets.all():
generated_assets = list(task.generated_assets.all())
for asset in generated_assets:
if asset.is_deleted:
continue
for f in asset.files.all():
@@ -706,7 +707,7 @@ def serialize_free_video_task(task: AITask) -> dict:
video_url = url
elif not f.is_primary and "image" in (f.content_type or "") and not thumbnail_url:
thumbnail_url = url
if not video_url:
if not video_url and not generated_assets:
video_url = payload.get("fallback_video_url") or ""
return {
+58 -1
View File
@@ -178,7 +178,7 @@ from django.test import TestCase
from rest_framework.test import APIClient
from apps.accounts.models import Team, TeamMember, User
from apps.ai.models import AITask, ImageConversation, ModelConfig
from apps.ai.models import AITask, ImageConversation, ModelConfig, ModelProvider
from apps.ai.providers.volcano import VolcanoArkProvider
from apps.ai.services import enqueue_standalone_images
from apps.assets.models import Asset, AssetFile
@@ -186,6 +186,63 @@ from apps.billing.models import CreditAccount
from apps.products.models import Product
class FreeVideoAssetDeletionTests(TestCase):
"""自由创作视频资产删除后,任务流应降级为无可播成品,不能回退到旧 fallback URL。"""
def setUp(self):
self.user = User.objects.create_user(username="fvdel", password="pass")
self.team = Team.objects.create(name="FreeVideoDel", owner=self.user)
TeamMember.objects.create(team=self.team, user=self.user, role="owner", status="active")
provider = ModelProvider.objects.create(name="fv-provider", display_name="FV Provider")
self.model = ModelConfig.objects.create(
provider=provider,
name="fv-model",
display_name="FV Model",
capability=ModelConfig.Capability.VIDEO,
)
self.task = AITask.objects.create(
team=self.team,
created_by=self.user,
task_type=AITask.Type.FREE_VIDEO,
status=AITask.Status.SUCCEEDED,
model_config=self.model,
idempotency_key="free-video-delete-test",
request_payload={
"prompt": "生成一个广告视频",
"fallback_video_url": "http://fallback.example/video.mp4",
"fallback_note": "fallback",
},
)
self.asset = Asset.objects.create(
team=self.team,
created_by=self.user,
name="自由创作视频",
asset_type=Asset.Type.VIDEO,
source=Asset.Source.AI_GENERATED,
category=Asset.Category.FREE_CREATE,
origin_task=self.task,
)
AssetFile.objects.create(
asset=self.asset,
object_key="free-video.mp4",
bucket="b",
content_type="video/mp4",
preview_url="http://tos.example/video.mp4",
is_primary=True,
)
def test_deleted_persisted_asset_does_not_fall_back_to_raw_video_url(self):
from apps.ai.free_video import serialize_free_video_task
self.assertEqual(serialize_free_video_task(self.task)["video_url"], "http://tos.example/video.mp4")
self.asset.is_deleted = True
self.asset.save(update_fields=["is_deleted"])
task = AITask.objects.prefetch_related("generated_assets", "generated_assets__files").get(id=self.task.id)
data = serialize_free_video_task(task)
self.assertEqual(data["video_url"], "")
self.assertEqual(data["fallback_note"], "fallback")
class StandaloneImageReferenceTests(TestCase):
"""独立生图(平台套图 / 模特上身图)必须把商品真实主图(+ 模特图)作为参考图走 image_edit,
而不是纯文生图——回归保护 #图片生成没参考商品主图# 这个 bug。
+78
View File
@@ -220,6 +220,84 @@ class AssetBatchesTests(TestCase):
self.assertEqual(self.client.get("/api/assets/summary/").json()["tryon"], 2)
class FreeCreateSplitTabTests(TestCase):
"""Free create assets share category=free_create; tabs split by asset_type."""
def setUp(self):
self.user, self.team = _mk_team("ufc", "TeamFC")
self.client = APIClient()
self.client.force_authenticate(self.user)
self.image1 = Asset.objects.create(
team=self.team,
name="free image 1",
asset_type=Asset.Type.IMAGE,
source=Asset.Source.AI_GENERATED,
category=Asset.Category.FREE_CREATE,
metadata={"batch_id": "IMG-B1"},
)
self.image2 = Asset.objects.create(
team=self.team,
name="free image 2",
asset_type=Asset.Type.IMAGE,
source=Asset.Source.AI_GENERATED,
category=Asset.Category.FREE_CREATE,
metadata={"batch_id": "IMG-B1"},
)
self.video = Asset.objects.create(
team=self.team,
name="free video",
asset_type=Asset.Type.VIDEO,
source=Asset.Source.AI_GENERATED,
category=Asset.Category.FREE_CREATE,
metadata={"feature": "free_video"},
)
def test_image_creations_batches_exclude_videos(self):
data = self.client.get("/api/assets/batches/?tab=image_creations&page_size=10").json()
self.assertEqual(data["count"], 1)
self.assertEqual(data["results"][0]["count"], 2)
self.assertEqual({a["asset_type"] for a in data["results"][0]["items"]}, {"image"})
def test_video_creations_list_excludes_images(self):
data = self.client.get("/api/assets/?tab=video_creations&page_size=10").json()
self.assertEqual(data["count"], 1)
self.assertEqual(data["results"][0]["id"], str(self.video.id))
self.assertEqual(data["results"][0]["asset_type"], "video")
def test_summary_exposes_split_counts_and_keeps_legacy_creations(self):
data = self.client.get("/api/assets/summary/").json()
self.assertEqual(data["image_creations"], 1)
self.assertEqual(data["video_creations"], 1)
self.assertEqual(data["creations"], 2)
def test_delete_and_restore_video_returns_to_video_creations_tab(self):
res = self.client.delete(f"/api/assets/{self.video.id}/")
self.assertEqual(res.status_code, 204)
self.video.refresh_from_db()
self.assertTrue(self.video.is_deleted)
video_tab = self.client.get("/api/assets/?tab=video_creations&page_size=10").json()
self.assertEqual(video_tab["count"], 0)
summary = self.client.get("/api/assets/summary/").json()
self.assertEqual(summary["video_creations"], 0)
self.assertEqual(summary["image_creations"], 1)
trash_ids = {a["id"] for a in self.client.get("/api/assets/trash/").json()["results"]}
self.assertIn(str(self.video.id), trash_ids)
restore = self.client.post(f"/api/assets/{self.video.id}/restore/")
self.assertEqual(restore.status_code, 200)
self.video.refresh_from_db()
self.assertFalse(self.video.is_deleted)
self.assertEqual(self.video.category, Asset.Category.FREE_CREATE)
self.assertEqual(self.video.asset_type, Asset.Type.VIDEO)
video_tab = self.client.get("/api/assets/?tab=video_creations&page_size=10").json()
self.assertEqual(video_tab["count"], 1)
self.assertEqual(video_tab["results"][0]["id"], str(self.video.id))
image_batches = self.client.get("/api/assets/batches/?tab=image_creations&page_size=10").json()
self.assertEqual({a["asset_type"] for a in image_batches["results"][0]["items"]}, {"image"})
class FacetsProductsTests(TestCase):
"""facets 返回本 tab 下真实关联的商品(?product= 三路之一:metadata.product_id 等),
并按 tab 隔离、team 隔离、只回 id/title。"""
+7 -2
View File
@@ -50,7 +50,11 @@ def _tab_q(tab: str) -> Q:
return Q(category="model_tryon")
if tab == "kits": # 平台套图(图片趴)
return Q(category="platform_kit")
if tab == "creations": # 自由创作(图片趴)
if tab == "image_creations": # 图片自由创作
return Q(category="free_create", asset_type="image")
if tab == "video_creations": # 视频自由创作
return Q(category="free_create", asset_type="video")
if tab == "creations": # 自由创作(兼容旧入口:图片+视频)
return Q(category="free_create")
if tab == "uploads":
return Q(category="upload")
@@ -64,7 +68,7 @@ def _tab_q(tab: str) -> Q:
# 图片趴三类按「生成批次」成组展示:同一次提交的 N 张图共享 metadata.batch_id。
BATCH_TABS = {"tryon", "kits", "creations"}
BATCH_TABS = {"tryon", "kits", "creations", "image_creations"}
# 资产名形如「AI 生成 · 模特上身图 · 4」,去掉尾部的「· 序号」得到批次名「AI 生成 · 模特上身图」。
_BATCH_NAME_RE = re.compile(r"\s*·\s*\d+\s*$")
@@ -180,6 +184,7 @@ class AssetViewSet(TeamScopedViewSetMixin, ModelViewSet):
for row in rows:
seen.add(_batch_key((row["metadata"] or {}).get("batch_id"), row["origin_task_id"], row["id"]))
out[t] = len(seen)
out["video_creations"] = base.filter(_tab_q("video_creations")).count()
out["others"] = base.filter(_tab_q("others")).count()
return Response(out)