后端(模特上身图提示词): - 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
122 lines
5.7 KiB
Python
122 lines
5.7 KiB
Python
#!/usr/bin/env python
|
|
"""故事板「画风锚点」对比 demo。
|
|
|
|
目的:直观演示"锁画风"——同一组分镜,各生成两版:
|
|
A. 无锚点(no_anchor) —— 模拟现状故事板提示词,只说"导演故事板…画面清晰",不规定画风
|
|
B. 有锚点(with_anchor)—— 同样内容 + 注入一段「统一画风锚点」(写实电商摄影棚风/统一色调光线构图)
|
|
|
|
每版各出 4 帧(钩子/痛点/卖点/CTA),存盘后肉眼对比:
|
|
- no_anchor 组:帧与帧画风易漂(这次写实、那次插画、景别色调各异)
|
|
- with_anchor 组:4 帧像同一套片子(统一风格)
|
|
|
|
走真·图像模型(yunqi/gpt-image-2,纯文生图,隔离出"锚点"这一个变量,不掺参考图)。
|
|
输出:../docs/storyboard-style-demo/{no_anchor,with_anchor}/frame{N}.png + prompts.md
|
|
|
|
用法:cd backend && python storyboard_style_demo.py
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
import django
|
|
|
|
BASE_DIR = Path(__file__).resolve().parent
|
|
sys.path.insert(0, str(BASE_DIR))
|
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "airshelf.settings.development")
|
|
django.setup()
|
|
|
|
from apps.ai.models import ModelConfig # noqa: E402
|
|
from apps.ai.providers import VolcanoArkProvider # noqa: E402 — media_to_bytes 复用
|
|
from apps.ai.services import build_provider, get_default_model # noqa: E402
|
|
|
|
OUT = BASE_DIR.parent / "docs" / "storyboard-style-demo"
|
|
SIZE = "1024x1536" # 9:16 竖屏,与故事板一致
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# 模拟数据:保温杯 4 分镜(钩子→痛点→卖点→CTA),每帧一句画面描述
|
|
# --------------------------------------------------------------------------- #
|
|
FRAMES = [
|
|
("钩子", "年轻女白领坐在办公室工位,皱眉摸了摸桌上凉掉的水杯,抬头看镜头一脸无奈"),
|
|
("痛点", "女白领从通勤包里拿出漏水的旧保温杯,纸巾擦被打湿的笔记本,表情懊恼"),
|
|
("卖点", "女白领单手按下保温杯一键弹盖,杯口冒出热气,桌面横放杯子滴水不漏"),
|
|
("CTA", "女白领手持保温杯对镜头微笑展示,画面右下角出现购物车引导点击"),
|
|
]
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# A. 无锚点:模拟现状故事板提示词(无任何画风约束)
|
|
# --------------------------------------------------------------------------- #
|
|
def prompt_no_anchor(role: str, scene: str) -> str:
|
|
return (
|
|
f"根据以下画面生成一个导演故事板分镜图,用于指导短视频生成。\n"
|
|
f"【镜头功能】{role}\n【画面】{scene}\n"
|
|
f"电商竖屏 9:16 导演故事板,画面清晰。"
|
|
)
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# B. 有锚点:同内容 + 一段「统一画风锚点」(每帧逐字相同,把风格焊死)
|
|
# --------------------------------------------------------------------------- #
|
|
STYLE_ANCHOR = (
|
|
"【统一画风 · 所有分镜必须严格一致,不可逐帧漂移】\n"
|
|
"· 风格:写实电商摄影棚实拍质感(photorealistic),禁止插画/线稿/漫画/3D 卡通;\n"
|
|
"· 布光:统一柔和影棚顺光,同一色温(暖白);\n"
|
|
"· 色彩:统一明亮干净的电商色彩分级,低饱和高级灰背景;\n"
|
|
"· 景别构图:统一中近景、人物居中、相同画面留白比例与镜头高度;\n"
|
|
"· 人物:全片同一位年轻女白领(同一张脸、同一发型妆容着装);\n"
|
|
"· 商品:全片同一只白色保温杯(同一外形/配色/logo)。"
|
|
)
|
|
|
|
|
|
def prompt_with_anchor(role: str, scene: str) -> str:
|
|
return (
|
|
f"根据以下画面生成一个导演故事板分镜图,用于指导短视频生成。\n"
|
|
f"【镜头功能】{role}\n【画面】{scene}\n"
|
|
f"{STYLE_ANCHOR}\n"
|
|
f"电商竖屏 9:16 导演故事板,画面清晰。"
|
|
)
|
|
|
|
|
|
def gen_one(provider, model, prompt: str, save_path: Path) -> str:
|
|
resp = provider.image_generation(model=model.name, endpoint=model.endpoint, prompt=prompt, size=SIZE)
|
|
media = provider.extract_first_media_url(resp)
|
|
fileobj, _ct = VolcanoArkProvider.media_to_bytes(media)
|
|
save_path.write_bytes(fileobj.getvalue())
|
|
return str(save_path)
|
|
|
|
|
|
def main() -> None:
|
|
model = get_default_model(ModelConfig.Capability.IMAGE)
|
|
provider = build_provider(model)
|
|
print(f"图像模型:{model.provider.name}/{model.name} · 尺寸 {SIZE}")
|
|
(OUT / "no_anchor").mkdir(parents=True, exist_ok=True)
|
|
(OUT / "with_anchor").mkdir(parents=True, exist_ok=True)
|
|
|
|
prompts_md = ["# 故事板画风锚点 demo · 用到的提示词\n",
|
|
f"> 图像模型:{model.provider.name}/{model.name} · 模拟商品:保温杯 · 4 分镜\n"]
|
|
|
|
for variant, builder in (("no_anchor", prompt_no_anchor), ("with_anchor", prompt_with_anchor)):
|
|
label = "无锚点(现状)" if variant == "no_anchor" else "有锚点(锁画风)"
|
|
print(f"\n===== {label} =====")
|
|
prompts_md.append(f"\n## {label}\n")
|
|
for i, (role, scene) in enumerate(FRAMES, 1):
|
|
prompt = builder(role, scene)
|
|
path = OUT / variant / f"frame{i}_{role}.png"
|
|
t0 = time.time()
|
|
try:
|
|
gen_one(provider, model, prompt, path)
|
|
print(f" ✅ 第{i}镜 {role} {round(time.time()-t0,1)}s → {path.name}")
|
|
except Exception as exc: # noqa: BLE001
|
|
print(f" ❌ 第{i}镜 {role} {str(exc)[:160]}")
|
|
prompts_md.append(f"\n### 第{i}镜 · {role}\n\n```\n{prompt}\n```\n")
|
|
|
|
(OUT / "prompts.md").write_text("\n".join(prompts_md), encoding="utf-8")
|
|
print(f"\n📁 图片+提示词已存:{OUT}")
|
|
print(" 对比:no_anchor/ 各帧画风易漂 vs with_anchor/ 4 帧同一套风格")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|