"""模特上身图 · 新提示词效果测试脚本(一次性,可删)。 设计:**提示词模板固定**(下面 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()