后端(模特上身图提示词): - 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
172 lines
8.2 KiB
Python
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()
|