Files
yingqing/core/backend/tryon_prompt_test.py
T
zyc de4b20cc7b 模特上身图提示词重构 + 图片创作页 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
2026-06-27 09:31:44 +08:00

172 lines
8.2 KiB
Python

"""模特上身图 · 新提示词效果测试脚本(一次性,可删)。
设计:**提示词模板固定**(下面 TEMPLATE/变化表/负面词都是常量),
**商品参数是变量**(按 PRODUCT_ID 从库里查出来填进模板)。
跑法:
.venv/bin/python tryon_prompt_test.py
产物:把生成图写到 OUT_DIR,顺带打印每张实际发出去的提示词。
"""
import os
import sys
import uuid
import django
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "airshelf.settings.development")
django.setup()
from apps.products.models import Product # noqa: E402
from apps.assets.models import Asset # noqa: E402
from apps.ai.models import ModelConfig # noqa: E402
from apps.ai.services import ( # noqa: E402
get_default_model,
get_image_provider,
_product_cover_url,
_asset_preview_url,
_ratio_to_image_size,
)
from apps.ai.providers.volcano import VolcanoArkProvider # noqa: E402
# ───────────────────────── 可调输入(变量) ─────────────────────────
PRODUCT_ID = "045ad8d6-8b15-486b-a4a9-f6968eb1555f" # 南卡 Lite Pro 蓝牙耳机
# 商品参考图(可多张):图1=真实上传(干净外形) + 图2=NANK 多角度(带品牌)。两张一起喂,锁外形+锁品牌。
PRODUCT_IMAGE_IDS = [
"c8c47a2e-6f07-43b2-b776-1be2a401538a", # 真实上传 · 图1
"463a826c-2adc-43c8-b776-2eea973f0449", # NANK 多角度(带 Logo)
]
MODEL_ID = "ad362810-cf3e-49c0-9426-8142a74b75b2" # 一位真人模特
RATIO = "3:4"
COUNT = 3 # 生成张数(每张动作/场景/镜头不同)
OUT_DIR = "/Users/maidong/Desktop/zyc/tryon_prompt_test2"
# ═════════════════════════ 固定提示词模板 ═════════════════════════
# 占位符全部由商品/模特变量填入;模板本身不随商品变。
TEMPLATE = (
"{product_intro}"
"请生成参考图{model_idx}中这位模特{relation}的电商详情页效果图。"
"模特要求:五官、发型、肤色、身形、年龄与气质必须与参考图{model_idx}(模特图)高度一致,不要换人,不要自行生成另一位模特。"
"商品要求:外形、配色、材质、品牌文字与 Logo、图案必须与{product_ref_label}(商品图)严格一致,不要重新设计、不要改样、不要生成相似款。"
"本张画面:{action};场景:{scene};镜头:{shot}。"
"画面风格:真实电商摄影,自然光,干净背景,主体突出,商品细节清晰,模特姿态自然,单人,高分辨率,{ratio} 构图。"
"(商品类目:{category};卖点:{selling_points}。以上文字仅供理解商品,不要据此凭空改变商品外观。)"
" 请规避:{negative}"
)
# 穿戴 / 非穿戴 两套「商品-模特关系」从句(固定)
RELATION_WEARABLE = (
"真实穿着参考图1中的这件商品,替换掉模特原本的衣服,让商品自然合身地穿在身上,"
"而不是把商品放在一旁展示;保持商品的版型、领口、袖型、长度、纹样不变"
)
RELATION_NON_WEARABLE = (
"自然地手持 / 在合适位置佩戴 / 正在使用参考图1中的这件商品(如为耳机则佩戴在耳朵上),"
"不要改动模特原本的服装,不要把商品强行穿到身上,商品以真实合理的方式出现在模特手中或身上"
)
# 每张按序号变化(固定的变化表;非穿戴默认)
VARIATIONS = [
{"action": "模特正面自然展示该商品", "scene": "干净的室内空间", "shot": "半身近景,商品清晰可见"},
{"action": "模特正在使用该商品", "scene": "生活化的真实居家场景", "shot": "侧面角度,突出使用方式"},
{"action": "模特手持并展示商品细节", "scene": "明亮的时尚生活场景", "shot": "中近景,商品占比较高"},
{"action": "模特与商品自然互动", "scene": "温暖时尚的生活场景", "shot": "半身,强调使用情境"},
]
NEGATIVE = (
"不要换人,不要改商品设计,不要改商品颜色与结构,不要生成错误或乱码的 Logo 与文字,"
"不要把商品改成相似款,不要多余的商品堆叠,不要多余文字,不要水印,不要边框,"
"不要低清模糊,不要过度磨皮,不要畸变,不要扭曲身体,不要夸张滤镜"
)
# 穿戴类类目关键词(命中即按穿戴从句)
WEARABLE_HINTS = ("服", "衣", "裤", "裙", "鞋", "帽", "袜", "围巾", "外套", "T恤", "卫衣", "内衣", "泳")
def is_wearable(category: str, title: str) -> bool:
blob = f"{category} {title}"
return any(h in blob for h in WEARABLE_HINTS)
def build_product_intro(name: str, n_product: int) -> tuple[str, int, str]:
"""据商品参考图数量自适应序号:N 张商品 → 参考图1~N=商品, 参考图N+1=模特。
返回 (intro, 模特图序号, 商品图序号标签)。"""
if n_product <= 1:
intro = f"参考图1是「{name}」的真实商品图,是该商品外观的唯一依据。参考图2是出镜模特。"
return intro, 2, "参考图1"
rng = f"1-{n_product}" if n_product > 2 else "1、2"
intro = (
f"参考图{rng}是「{name}」同一件真实商品的不同角度图,是该商品外观的唯一依据,"
f"请综合这些角度还原商品。参考图{n_product + 1}是出镜模特。"
)
return intro, n_product + 1, f"参考图{rng}"
def build_prompt(product, index: int, ratio: str, n_product: int) -> str:
name = (product.title or "商品").strip()
category = (product.category or "").strip()
points = list(product.selling_points.all().values_list("title", flat=True))
selling = "、".join(points) if points else "(无)"
relation = RELATION_WEARABLE if is_wearable(category, name) else RELATION_NON_WEARABLE
var = VARIATIONS[index % len(VARIATIONS)]
product_intro, model_idx, product_ref_label = build_product_intro(name, n_product)
return TEMPLATE.format(
product_intro=product_intro,
model_idx=model_idx,
product_ref_label=product_ref_label,
relation=relation,
action=var["action"],
scene=var["scene"],
shot=var["shot"],
ratio=ratio,
category=category or "(未填)",
selling_points=selling,
negative=NEGATIVE,
)
def main():
os.makedirs(OUT_DIR, exist_ok=True)
product = Product.objects.get(id=PRODUCT_ID)
model_asset = Asset.objects.get(id=MODEL_ID)
product_urls = []
for aid in PRODUCT_IMAGE_IDS:
a = Asset.objects.get(id=aid)
u = _asset_preview_url(a)
if u:
product_urls.append(u)
print(f"商品参考图: {a.name} | {a.source} | {u[:80]}")
model_url = _asset_preview_url(model_asset)
if not product_urls:
print("❌ 没有可用的商品参考图。")
sys.exit(1)
n_product = len(product_urls)
print(f"商品: {product.title} | 类目: {product.category} | 穿戴类: {is_wearable(product.category or '', product.title)} | 商品参考图数: {n_product}")
print(f"模特: {model_asset.name} | {model_url[:80]}")
model_config = get_default_model(ModelConfig.Capability.IMAGE)
provider = get_image_provider(model_config)
print(f"模型: {model_config.provider.name}:{model_config.name}\n")
images = product_urls + [model_url] # 参考图1~N=商品多角度, 参考图N+1=模特
size = _ratio_to_image_size(RATIO)
for i in range(COUNT):
prompt = build_prompt(product, i, RATIO, n_product)
print(f"━━━ 第 {i + 1} 张 ━━━\n{prompt}\n")
try:
resp = provider.image_edit(model=model_config.name, prompt=prompt, images=images, size=size)
media = provider.extract_first_media_url(resp)
fileobj, content_type = VolcanoArkProvider.media_to_bytes(media)
ext = ".jpg" if "jpeg" in content_type else (".webp" if "webp" in content_type else ".png")
path = os.path.join(OUT_DIR, f"tryon_{i + 1}{ext}")
with open(path, "wb") as f:
f.write(fileobj.getvalue())
print(f"✅ 第 {i + 1} 张 → {path}\n")
except Exception as exc: # noqa: BLE001
print(f"❌ 第 {i + 1} 张失败: {exc}\n")
print(f"完成。产物目录: {OUT_DIR}")
if __name__ == "__main__":
main()