fix(core): 商品详情页三视图改走 image_edit 参考真实主图 + 该商品 AI 素材归属
- 商品详情页「AI 生成三视图」原走独立生图老链路(纯文生图),只凭商品名脑补, 出来通用图不像真品。改:前端传 reference_product;后端 run_standalone_image_task 据此取商品主图走 image_edit(复用 build_product_triview_prompt_refs 锁包装), 取不到主图优雅回落文生图。图片创作页(自由创作)不带标记,不受影响。 - 商品详情页素材区改为只显示「该商品」AI 素材(asset.product 归属)而非全团队。 - 首登水合带重试,失败不再静默(否则页面卡在全 0,要刷新才好)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1334,7 +1334,7 @@ def _reap_stale_standalone_image_tasks(*, team) -> None:
|
||||
continue
|
||||
|
||||
|
||||
def enqueue_standalone_images(*, team, user, prompt: str, mode: str = "image", count: int = 1) -> list[AITask]:
|
||||
def enqueue_standalone_images(*, team, user, prompt: str, mode: str = "image", count: int = 1, product_id: str | None = None, reference_product: bool = False) -> list[AITask]:
|
||||
"""独立生图(图片创作 / 模特上身图 / 平台套图)改为**异步**:本函数在 Web 请求里只做「建任务 +
|
||||
预留额度」这种秒级的活,真正 ~30s 的 ARK 出图交给 Celery worker(generate_standalone_image_task)。
|
||||
|
||||
@@ -1360,7 +1360,7 @@ def enqueue_standalone_images(*, team, user, prompt: str, mode: str = "image", c
|
||||
status=AITask.Status.CREATED,
|
||||
model_config=model_config,
|
||||
idempotency_key=f"standalone-image:{team.id}:{uuid.uuid4()}",
|
||||
request_payload={"model": model_config.name, "endpoint": model_config.endpoint, "prompt": prompt, "mode": mode, "index": index},
|
||||
request_payload={"model": model_config.name, "endpoint": model_config.endpoint, "prompt": prompt, "mode": mode, "index": index, "product_id": str(product_id) if product_id else None, "reference_product": bool(reference_product)},
|
||||
estimated_cost=cost,
|
||||
)
|
||||
# 预留额度若余额不足会抛 ValueError,在同步的 Web 请求里立刻反馈给前端(不会先建半套任务)
|
||||
@@ -1386,12 +1386,28 @@ def run_standalone_image_task(*, task_id: str) -> None:
|
||||
prompt = str(payload.get("prompt") or "")
|
||||
mode = str(payload.get("mode") or "image")
|
||||
index = int(payload.get("index") or 0)
|
||||
product_id = payload.get("product_id") or None
|
||||
category = _STANDALONE_CATEGORY.get(mode, Asset.Category.UNCATEGORIZED)
|
||||
model_config = task.model_config
|
||||
provider = get_image_provider(model_config)
|
||||
reservation = task.credit_reservation
|
||||
# 商品三视图(商品详情页按 reference_product 提交):有真实商品主图 + 模型支持 image_edit →
|
||||
# 以主图为参考锁包装一致性,否则回落纯文生图(仅凭商品名脑补,不保证还原真实包装)。
|
||||
ref_url = ""
|
||||
product = None
|
||||
if bool(payload.get("reference_product")) and product_id:
|
||||
from apps.products.models import Product
|
||||
|
||||
product = Product.objects.filter(id=product_id).first()
|
||||
if product is not None:
|
||||
ref_url = _product_cover_url(product)
|
||||
use_edit = bool(ref_url) and hasattr(provider, "image_edit")
|
||||
try:
|
||||
response = provider.image_generation(model=model_config.name, endpoint=model_config.endpoint, prompt=prompt)
|
||||
if use_edit:
|
||||
edit_prompt = build_product_triview_prompt_refs(product, "")
|
||||
response = provider.image_edit(model=model_config.name, prompt=edit_prompt, images=[ref_url], size="1536x1024")
|
||||
else:
|
||||
response = provider.image_generation(model=model_config.name, endpoint=model_config.endpoint, prompt=prompt)
|
||||
media = provider.extract_first_media_url(response)
|
||||
with transaction.atomic():
|
||||
task.status = AITask.Status.SUCCEEDED
|
||||
@@ -1408,6 +1424,8 @@ def run_standalone_image_task(*, task_id: str) -> None:
|
||||
asset = Asset.objects.create(
|
||||
id=asset_id, team=team, created_by=user, name=f"AI 生成 · {mode} · {index + 1}",
|
||||
asset_type=Asset.Type.IMAGE, source=Asset.Source.AI_GENERATED, category=category, origin_task=task,
|
||||
# 记下生图时选中的商品,商品详情页据此只展示「该商品」的 AI 素材(而非全团队)
|
||||
metadata={"product_id": str(product_id)} if product_id else {},
|
||||
)
|
||||
AssetFile.objects.create(asset=asset, object_key=stored.object_key, bucket=stored.bucket, content_type=stored.content_type, size_bytes=stored.size_bytes, is_primary=True)
|
||||
except Exception as exc: # noqa: BLE001 — 失败要退费并把错误记进 AITask 供前端轮询读取;不向上抛(避免 celery 重试二次扣费)
|
||||
|
||||
@@ -29,9 +29,11 @@ class GenerateImageView(APIView):
|
||||
count = int(request.data.get("count") or 1)
|
||||
except (TypeError, ValueError):
|
||||
count = 1
|
||||
product_id = str(request.data.get("product_id") or "").strip() or None
|
||||
reference_product = bool(request.data.get("reference_product"))
|
||||
team = get_current_team(request.user)
|
||||
try:
|
||||
tasks = enqueue_standalone_images(team=team, user=request.user, prompt=prompt, mode=mode, count=count)
|
||||
tasks = enqueue_standalone_images(team=team, user=request.user, prompt=prompt, mode=mode, count=count, product_id=product_id, reference_product=reference_product)
|
||||
except ValueError as exc: # 无可用模型 / 余额不足等,立即反馈
|
||||
return Response({"detail": str(exc)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response(
|
||||
|
||||
@@ -58,6 +58,21 @@ class AssetFileSerializer(serializers.ModelSerializer):
|
||||
|
||||
class AssetSerializer(serializers.ModelSerializer):
|
||||
files = AssetFileSerializer(many=True, read_only=True)
|
||||
# 资产归属商品(只读):商品详情页据此只展示「该商品」的 AI 素材,而非全团队
|
||||
product = serializers.SerializerMethodField()
|
||||
|
||||
def get_product(self, obj):
|
||||
"""解析资产所属商品:
|
||||
1) 独立生图(图片创作/模特图/平台套图):生图时已写入 metadata.product_id;
|
||||
2) 项目内生成(基础资产/分镜图等):回溯 origin_task → project → product。
|
||||
两条路都拿不到则归属 None(如纯手动上传到素材库、与任何商品无关的图)。"""
|
||||
pid = (obj.metadata or {}).get("product_id")
|
||||
if pid:
|
||||
return str(pid)
|
||||
task = obj.origin_task
|
||||
if task and task.project_id and task.project.product_id:
|
||||
return str(task.project.product_id)
|
||||
return None
|
||||
|
||||
class Meta:
|
||||
model = Asset
|
||||
@@ -71,6 +86,7 @@ class AssetSerializer(serializers.ModelSerializer):
|
||||
"metadata",
|
||||
"is_deleted",
|
||||
"origin_task",
|
||||
"product",
|
||||
"files",
|
||||
"review_status",
|
||||
"review_error",
|
||||
|
||||
@@ -28,7 +28,8 @@ class AssetPagination(PageNumberPagination):
|
||||
|
||||
|
||||
class AssetViewSet(TeamScopedViewSetMixin, ModelViewSet):
|
||||
queryset = Asset.objects.prefetch_related("files").all()
|
||||
# select_related 回溯链:asset → origin_task → project,供序列化器解析资产归属商品(避免 N+1)
|
||||
queryset = Asset.objects.prefetch_related("files").select_related("origin_task__project").all()
|
||||
serializer_class = AssetSerializer
|
||||
pagination_class = AssetPagination
|
||||
search_fields = ["name", "description"]
|
||||
|
||||
Reference in New Issue
Block a user