fix(ai): 平台套图改走商品主图参考 image_edit · 模特上身图锁商品+模特双图

- 平台套图(cover)以前纯文生图,不参考真实商品主图 → 出图与商品对不上;
  现以商品主图为参考图1走 image_edit,锁包装一致性,有模特则参考图2=模特
- 新增 build_platform_cover_prompt_refs;model_url 解析对 cover 模式同样生效
- 补回归测试:cover 传商品主图、model 传商品主图+模特图、无主图回落文生图

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
zyc
2026-06-17 21:58:39 +08:00
co-authored by Claude Opus 4.8
parent 7c6d4477da
commit 0695c36f4c
12 changed files with 455 additions and 126 deletions
+75
View File
@@ -93,3 +93,78 @@ class NormalizeDraftTests(SimpleTestCase):
self.assertEqual(len(draft["segments"]), 2)
self.assertEqual(draft["segments"][0]["narration"], "口播1")
self.assertEqual(draft["segments"][1]["visual"], "画面2")
from io import BytesIO
from unittest.mock import patch
from django.test import TestCase
from apps.accounts.models import Team, User
from apps.ai.services import enqueue_standalone_images
from apps.assets.models import Asset, AssetFile
from apps.billing.models import CreditAccount
from apps.products.models import Product
class StandaloneImageReferenceTests(TestCase):
"""独立生图(平台套图 / 模特上身图)必须把商品真实主图(+ 模特图)作为参考图走 image_edit,
而不是纯文生图——回归保护 #图片生成没参考商品主图# 这个 bug。
(图像默认模型由迁移 seed 的 tokenssr:gpt-image-2 提供,get_image_provider 全程 mock。)"""
def setUp(self):
self.user = User.objects.create_user(username="owner", password="pass")
self.team = Team.objects.create(name="T", owner=self.user)
CreditAccount.objects.create(team=self.team, balance="100.0000")
# 商品 + 主图(带可访问 preview_url)
self.product = Product.objects.create(team=self.team, created_by=self.user, title="南卡 Lite Pro")
cover = Asset.objects.create(
team=self.team, created_by=self.user, name="主图", asset_type=Asset.Type.IMAGE,
source=Asset.Source.UPLOAD, category=Asset.Category.PRODUCT_IMAGE,
)
AssetFile.objects.create(asset=cover, object_key="c.png", bucket="b", content_type="image/png", preview_url="http://x/cover.png", is_primary=True)
self.product.cover_asset = cover
self.product.save(update_fields=["cover_asset"])
def _patch_provider(self):
# image_edit 返回值 + 媒体落库链路全部 mock,聚焦验证「传了哪些参考图」
provider = patch("apps.ai.services.get_image_provider").start()
prov = provider.return_value
prov.image_edit.return_value = {"data": [{"url": "http://x/out.png"}]}
prov.image_generation.return_value = {"data": [{"url": "http://x/out.png"}]}
prov.extract_first_media_url.return_value = "http://x/out.png"
media = patch("apps.ai.services.VolcanoArkProvider.media_to_bytes").start()
media.return_value = (BytesIO(b"img"), "image/png")
store = patch("apps.ai.services.TosStorage").start()
stored = store.return_value.upload_fileobj.return_value
stored.object_key, stored.bucket, stored.content_type, stored.size_bytes = "o.png", "b", "image/png", 3
self.addCleanup(patch.stopall)
return prov
def test_cover_mode_references_product_main_image(self):
prov = self._patch_provider()
enqueue_standalone_images(team=self.team, user=self.user, prompt="平台套图", mode="cover", count=1, product_id=str(self.product.id), ratio="4:5")
prov.image_edit.assert_called_once()
self.assertEqual(prov.image_edit.call_args.kwargs["images"], ["http://x/cover.png"])
prov.image_generation.assert_not_called()
def test_model_tryon_combines_product_and_model_images(self):
prov = self._patch_provider()
# 模特资产(PERSON)带 preview_url
model_asset = Asset.objects.create(
team=self.team, created_by=self.user, name="模特", asset_type=Asset.Type.IMAGE,
source=Asset.Source.AI_GENERATED, category=Asset.Category.PERSON,
)
AssetFile.objects.create(asset=model_asset, object_key="m.png", bucket="b", content_type="image/png", preview_url="http://x/model.png", is_primary=True)
enqueue_standalone_images(team=self.team, user=self.user, prompt="上身图", mode="model", count=1, product_id=str(self.product.id), model_id=str(model_asset.id), ratio="4:5")
prov.image_edit.assert_called_once()
self.assertEqual(prov.image_edit.call_args.kwargs["images"], ["http://x/cover.png", "http://x/model.png"])
prov.image_generation.assert_not_called()
def test_cover_mode_falls_back_to_t2i_without_main_image(self):
prov = self._patch_provider()
self.product.cover_asset = None
self.product.save(update_fields=["cover_asset"])
enqueue_standalone_images(team=self.team, user=self.user, prompt="平台套图", mode="cover", count=1, product_id=str(self.product.id), ratio="4:5")
prov.image_generation.assert_called_once()
prov.image_edit.assert_not_called()