模特上身图提示词重构 + 图片创作页 UI 调整
后端(模特上身图提示词): - build_model_tryon_prompt_refs 重写:穿戴/非穿戴分流(穿戴=真实穿身替换原衣, 非穿戴=手持/佩戴/使用不动原衣)、每张按 index 变化动作/场景/镜头、负面词尾接、 多图参考序号自适应(参考图1~N=商品,参考图N+1=模特) - 新增 _product_reference_urls:商品参考图真实上传图优先、排除 AI 生成图、可多张(≤3), 无真实图回落 cover - worker run_standalone_image_task 模特分支改用多图取图 + 传 index/n_product 前端(图片创作/工作室): - 生成数量改 1/2/4;图片比例新增「手动输入」(宽:高 两输入框) - 临时隐藏「商品库」按钮 - 模特卡:去掉 // 真人模特,标题改图片底部白字遮罩层 + 单行省略 - 工作室壳负边距对齐 .content padding,修复上下被遮挡/裁切 其他:并入此前未提交的商品页改动、脚本 Agent/格式实测文档与 demo
This commit is contained in:
@@ -752,6 +752,35 @@ def _product_cover_url(product) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
def _product_reference_urls(product, limit: int = 3) -> list[str]:
|
||||
"""模特上身图的商品参考图(可多张):**真实上传图优先,排除 AI 生成图**——避免拿生成图当真相
|
||||
再喂回模型造成误差累积。按主图/排序取前 limit 张真实上传图;一张都没有时回落 cover(即便是 AI 图,
|
||||
至少保证能走 image_edit 而不是纯文生图)。"""
|
||||
if product is None:
|
||||
return []
|
||||
from apps.assets.models import Asset
|
||||
|
||||
urls: list[str] = []
|
||||
seen: set[str] = set()
|
||||
rels = list(product.images.select_related("asset").all())
|
||||
rels.sort(key=lambda im: (not im.is_primary, im.sort_order))
|
||||
for im in rels:
|
||||
a = im.asset
|
||||
if a is None or getattr(a, "source", "") == Asset.Source.AI_GENERATED:
|
||||
continue
|
||||
u = _asset_preview_url(a)
|
||||
if u and u not in seen:
|
||||
seen.add(u)
|
||||
urls.append(u)
|
||||
if len(urls) >= limit:
|
||||
return urls
|
||||
if not urls: # 无任何真实上传图 → 回落 cover(可能是 AI 图,但好过纯文生图)
|
||||
cover = _product_cover_url(product)
|
||||
if cover:
|
||||
urls.append(cover)
|
||||
return urls
|
||||
|
||||
|
||||
def quality_words(stage: str, slot: str = "quality") -> list[str]:
|
||||
"""平台单层质量词配置(QualityWord)。无配置/表不存在 → 返回 [],调用方回落写死值,保证零回归。"""
|
||||
try:
|
||||
@@ -815,26 +844,69 @@ def build_product_triview_prompt_refs(product, base_prompt: str = "") -> str:
|
||||
return render_prompt("product_triview", default, 商品=name, 补充=(base_prompt or "").strip())
|
||||
|
||||
|
||||
def build_model_tryon_prompt_refs(product, has_model: bool, base_prompt: str = "") -> str:
|
||||
# 模特上身图:按品类分「穿戴 / 非穿戴」注入不同的商品-模特关系;每张按序号变化动作/场景/镜头。
|
||||
_TRYON_WEARABLE_HINTS = ("服", "衣", "裤", "裙", "鞋", "帽", "袜", "围巾", "外套", "卫衣", "内衣", "文胸", "胸罩", "泳", "bra")
|
||||
_TRYON_VARIATIONS = [
|
||||
{"action": "模特正面自然展示该商品", "scene": "干净的室内空间", "shot": "半身近景,商品清晰可见"},
|
||||
{"action": "模特正在穿着 / 使用该商品", "scene": "生活化的真实居家场景", "shot": "侧面角度,突出穿着或使用方式"},
|
||||
{"action": "模特手持或局部展示商品细节", "scene": "明亮的时尚生活场景", "shot": "中近景,商品占比较高"},
|
||||
{"action": "模特与商品自然互动", "scene": "温暖时尚的生活场景", "shot": "半身,强调使用情境"},
|
||||
]
|
||||
_TRYON_NEGATIVE = (
|
||||
"不要换人,不要改商品设计,不要改商品颜色与结构,不要生成错误或乱码的 Logo 与文字,"
|
||||
"不要把商品改成相似款,不要多余的商品堆叠,不要多余文字,不要水印,不要边框,"
|
||||
"不要低清模糊,不要过度磨皮,不要畸变,不要扭曲身体,不要夸张滤镜"
|
||||
)
|
||||
|
||||
|
||||
def _is_wearable_product(product) -> bool:
|
||||
"""据类目/标题判断是否「穿戴类」(服饰鞋帽内衣) → 真实穿到身上;否则非穿戴 → 手持/佩戴/使用。"""
|
||||
blob = f"{getattr(product, 'category', '') or ''} {getattr(product, 'title', '') or ''}".lower()
|
||||
return any(h in blob for h in _TRYON_WEARABLE_HINTS)
|
||||
|
||||
|
||||
def build_model_tryon_prompt_refs(product, has_model: bool, base_prompt: str = "", index: int = 0, n_product: int = 1) -> str:
|
||||
"""模特上身图 image_edit 提示词(refs 版):
|
||||
参考图1=商品真实主图(锁商品外形/品牌/配色),参考图2=选中模特(锁人脸/身形/气质)。
|
||||
生成「该模特自然展示/使用该商品」的电商效果图。"""
|
||||
参考图1~N=商品真实图(多角度,锁外形/品牌/配色),参考图N+1=选中模特(锁人脸/身形/气质)。
|
||||
按品类分穿戴/非穿戴(穿戴=真实穿身上、替换原衣;非穿戴=手持/佩戴/使用,不动原衣);
|
||||
`index` 让每张图动作/场景/镜头不同;`n_product` 让参考图序号自适应。"""
|
||||
name = (getattr(product, "title", "") or "商品").strip()
|
||||
lines = [f"参考图1是「{name}」的真实商品。"]
|
||||
if has_model:
|
||||
lines += [
|
||||
"参考图2是出镜模特。请生成参考图2中的这位模特自然地展示/佩戴/使用参考图1中商品的电商效果图。",
|
||||
"模特的五官、发型、肤色、身形与气质必须与参考图2高度一致,不要换人;",
|
||||
"商品的外形、品牌文字、配色、Logo 必须与参考图1高度一致,不要改动或重新设计。",
|
||||
]
|
||||
n_product = max(1, int(n_product or 1))
|
||||
# 参考图序号自适应:N 张商品图 → 参考图1~N=商品, 参考图N+1=模特
|
||||
if n_product <= 1:
|
||||
intro = f"参考图1是「{name}」的真实商品图,是该商品外观的唯一依据。"
|
||||
prod_ref = "参考图1"
|
||||
model_idx = 2
|
||||
else:
|
||||
lines += [
|
||||
"请生成一位真人模特自然地展示/佩戴/使用参考图1中商品的电商效果图。",
|
||||
"商品的外形、品牌文字、配色、Logo 必须与参考图1高度一致,不要改动或重新设计。",
|
||||
]
|
||||
rng = f"1-{n_product}" if n_product > 2 else "1、2"
|
||||
intro = f"参考图{rng}是「{name}」同一件真实商品的不同角度图,是该商品外观的唯一依据,请综合这些角度还原商品。"
|
||||
prod_ref = f"参考图{rng}"
|
||||
model_idx = n_product + 1
|
||||
|
||||
if _is_wearable_product(product):
|
||||
relation = (
|
||||
f"真实穿着{prod_ref}中的这件商品,替换掉模特原本的衣服,让商品自然合身地穿在身上,"
|
||||
"而不是放在一旁展示,保持商品的版型、领口、袖型、长度、纹样不变"
|
||||
)
|
||||
else:
|
||||
relation = (
|
||||
f"自然地手持 / 在合适位置佩戴 / 正在使用{prod_ref}中的这件商品(如为耳机则佩戴在耳朵上),"
|
||||
"不要改动模特原本的服装,不要把商品强行穿到身上"
|
||||
)
|
||||
|
||||
var = _TRYON_VARIATIONS[index % len(_TRYON_VARIATIONS)]
|
||||
lines = [intro]
|
||||
if has_model:
|
||||
lines.append(f"参考图{model_idx}是出镜模特。请生成参考图{model_idx}中这位模特{relation}的电商详情页效果图。")
|
||||
lines.append(f"模特的五官、发型、肤色、身形、年龄与气质必须与参考图{model_idx}(模特图)高度一致,不要换人,不要自行生成另一位模特。")
|
||||
else:
|
||||
lines.append(f"请生成一位真人模特{relation}的电商详情页效果图。")
|
||||
lines.append(f"商品的外形、配色、材质、品牌文字与 Logo、图案必须与{prod_ref}(商品图)严格一致,不要重新设计、不要改样、不要生成相似款。")
|
||||
lines.append(f"本张画面:{var['action']};场景:{var['scene']};镜头:{var['shot']}。")
|
||||
lines.append(quality_suffix("model_tryon", "自然光、真实质感、干净背景、电商主图构图,人物与商品比例真实协调。"))
|
||||
if base_prompt and base_prompt.strip():
|
||||
lines.append(base_prompt.strip())
|
||||
lines.append("请规避:" + _TRYON_NEGATIVE)
|
||||
return " ".join(lines)
|
||||
|
||||
|
||||
@@ -1985,13 +2057,18 @@ def run_standalone_image_task(*, task_id: str) -> None:
|
||||
if model_asset is not None:
|
||||
model_url = _asset_preview_url(model_asset)
|
||||
product_url = _product_cover_url(product) if product is not None else ""
|
||||
# 模特上身图:真实上传图优先、排除 AI 生成图,可多张(多角度更易锁外形/品牌)
|
||||
product_urls = _product_reference_urls(product, limit=3) if product is not None else []
|
||||
|
||||
edit_images: list[str] = []
|
||||
edit_prompt = ""
|
||||
if mode == "model" and can_edit and product_url:
|
||||
# 模特上身图:商品图必有,模特图可缺(缺则让模型自取真人模特)
|
||||
edit_images = [product_url] + ([model_url] if model_url else [])
|
||||
edit_prompt = build_model_tryon_prompt_refs(product, has_model=bool(model_url), base_prompt=prompt)
|
||||
if mode == "model" and can_edit and product_urls:
|
||||
# 模特上身图:参考图1~N=商品真实图(多角度),参考图N+1=模特(模特图可缺则让模型自取真人模特)
|
||||
edit_images = product_urls + ([model_url] if model_url else [])
|
||||
edit_prompt = build_model_tryon_prompt_refs(
|
||||
product, has_model=bool(model_url), base_prompt=prompt,
|
||||
index=index, n_product=len(product_urls),
|
||||
)
|
||||
elif mode == "cover" and can_edit and product_url:
|
||||
# 平台套图:参考图1=商品真实主图(锁包装一致性),有模特则参考图2=模特(锁人脸/身形)
|
||||
edit_images = [product_url] + ([model_url] if model_url else [])
|
||||
|
||||
@@ -57,17 +57,24 @@ class ProductViewSet(TeamScopedViewSetMixin, ModelViewSet):
|
||||
|
||||
product = self.get_object()
|
||||
team = product.team
|
||||
assets = list(
|
||||
# 真因不是 OR,而是 select_related("origin_task__project") 把每个资产关联的 AITask 整行拉过来,
|
||||
# 含 request_payload / response_payload 两个巨型 JSON 列(实测 114 行就要 ~66s 纯传输)。
|
||||
# 视图与序列化器只用到 origin_task.project(取项目名/id),从不读 payload → .defer() 掉这两列。
|
||||
base = (
|
||||
Asset.objects.filter(team=team, is_deleted=False)
|
||||
.filter(
|
||||
Q(metadata__product_id=str(product.id))
|
||||
| Q(origin_task__project__product_id=product.id)
|
||||
| Q(product_images__product_id=product.id)
|
||||
)
|
||||
.select_related("origin_task__project")
|
||||
.defer("origin_task__request_payload", "origin_task__response_payload")
|
||||
.prefetch_related("files")
|
||||
.distinct()
|
||||
)
|
||||
by_id = {}
|
||||
for qs in (
|
||||
base.filter(metadata__product_id=str(product.id)),
|
||||
base.filter(origin_task__project__product_id=product.id),
|
||||
base.filter(product_images__product_id=product.id),
|
||||
):
|
||||
for a in qs:
|
||||
by_id[a.id] = a
|
||||
assets = sorted(by_id.values(), key=lambda a: (a.created_at is not None, a.created_at), reverse=True)
|
||||
|
||||
def ser(a):
|
||||
return AssetSerializer(a).data
|
||||
|
||||
Reference in New Issue
Block a user