Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ node_modules/
*.local
__pycache__/
memory/
out/
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,19 @@ cp -r ecom-prompt-sop/ecom-prompt-sop ~/.agents/skills/
| `evals/check_cases.py` | 校验 `cases.json` 与 `trigger-cases.md` 一致,并确认预期 SOP 路径存在 |
| `evals/promptfooconfig.yaml` | 触发回归:断言每条提示命中的技能名与 SOP 路径 |
| `evals/promptfooconfig.quality.yaml` | 文本质量抽检:读取 SOP 原文生成提示词,按硬规则评分;图片输出仍由人工检查 |
| `tools/qwen_image.py` | 命令行出图通道:提示词直接投喂 Qwen-Image 2.1 Pro,支持文生图与最多 10 张参考图的图生图 |

## 云端出图(Qwen-Image 2.1 Pro)

`tools/qwen_image.py` 走阿里云百炼 DashScope 同步接口,把 SOP 产出的提示词直接变成图片,省去手工复制到网页端。密钥只从环境变量 `DASHSCOPE_API_KEY` 或 `--key-file` 读取,不写入仓库任何文件:

```bash
export DASHSCOPE_API_KEY="sk-..."
python tools/qwen_image.py -p "提示词" --size 1024*1024 -o out
python tools/qwen_image.py -f prompt.txt -i front.jpg -i detail.jpg -n 2 --seed 42
```

图生图用于产品锁定和翻新:把产品实拍图作为参考图,提示词里写清保留清单与改动清单。提示词默认按原文投喂、不做模型改写,SOP 里的硬规则不会被稀释;只有提示词很短、希望模型自行扩写时才加 `--prompt-extend`。`-n` 最多 6 张,`--size` 支持 `auto` 或 `宽*高`,输出落在 `out/`(已在 `.gitignore` 中忽略)。`python tools/qwen_image.py --selftest` 可离线校验参数与响应解析。

## 评测与 CI

Expand Down
17 changes: 16 additions & 1 deletion ecom-detail-page-prompts/references/tool-adaptation.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
| GPT-Image(2 / 2.5 系) | 中文渲染较强,长文本准确率下降;文字用引号包裹、指定字体可提升 | 好,跨图商品一致性较好 | 分层结构:任务/主体/结构约束/光线材质/输出边界 | 无负词字段,用"输出边界"句 |
| Seedream(即梦 4/5 系) | 中文海报文字可直接用,生僻字偶有出错 | 支持十图级输入 | 官方建议自然语言连贯描述加短词美学,避免复杂堆词 | 无负词字段 |
| 可灵 Kling(图片 3 / O1 系) | 中文生态内表现好 | 支持主体一致性控制与多图参考 | 对话式、编辑式,适合同主体多轮迭代 | 自然语言句 |
| Qwen-Image 系 | 强化汉字渲染 | 支持 | 原生支持长指令(千 token 级),中文长 SOP 可直接投喂 | 自然语言句 |
| Qwen-Image 2.1 Pro / 3.0 系 | 强化汉字渲染,短标题稳定 | 2.1 系支持 1 到 10 张参考图;3.0 系 1 到 3 张 | 原生支持长指令(千 token 级),中文长 SOP 可直接投喂 | 2.1 系无负词字段,用自然语言边界句;3.0 系支持 negative_prompt |
| FLUX 系(本地/开源) | 文本渲染持续改善 | 支持多参考图 | 关键词与结构化描述均可 | 保留种子、负词、LoRA 等精细控制 |

多项为社区实测结论,工具版本迭代快,以官方为准。
Expand All @@ -22,6 +22,7 @@
- **Seedream(即梦)**:把结构化清单改写成连贯自然语言句子,保留短词审美描述,删掉堆叠形容词。
- **GPT-Image**:按分层结构组织——任务、主体、结构约束、光线与材质、输出边界,逐层成段。
- **Gemini**:对话式写法——先给参考图与整体要求,图内文字用引号包裹,随后可给多轮修图步骤。
- **Qwen-Image 2.1 Pro**:完整页面提示词可直接投喂。逐页生成时每页单独调用,参考图按「产品主图在前、细节图在后」排序;脚本默认不改写提示词,避免模型重写破坏版式与文字锁定描述。
- **FLUX 等本地模型**:可附种子与负面提示词列表,便于批量复现。

通用版写法(未指定工具时):先给产品与页面主题,再给构图与版式结构,然后背景、光影、装饰系统,最后主副标题与信息模块文字、输出边界收尾;一页提示词内保持同一套层级顺序。
Expand Down Expand Up @@ -58,3 +59,17 @@
- **TikTok Shop**:AI 生成内容须披露(画面标注或发布开关)。
- **中国法规**:AI 生成合成内容须显式与隐式双标识。
- 纯产品详情页(无 AI 人物)不涉及人物标注,但商品外观仍须与参考图一致,严禁货不对板。

## 六、本仓库命令行出图通道(Qwen-Image 2.1 Pro)

仓库内 `tools/qwen_image.py` 提供文生图与图生图命令行通道,走阿里云百炼 DashScope 同步接口,模型 `qwen-image-2.1-pro`。密钥只从环境变量 `DASHSCOPE_API_KEY` 或 `--key-file` 读取,不写入任何文件。

```bash
export DASHSCOPE_API_KEY="sk-..." # 也可用 --key-file 指向密钥文件
python tools/qwen_image.py -p "提示词" -o out
python tools/qwen_image.py -f page03.txt -i product_front.jpg -i product_detail.jpg --size 1024*1536
```

参数要点:`--size` 用 `宽*高`(也接受 `宽x高`)或 `auto`;`-n` 支持 1 到 6 张;`--seed` 固定随机种子;`-i` 可重复,最多 10 张参考图。提示词默认按原文投喂、不做模型改写,需要模型按简单描述扩写时加 `--prompt-extend`。输出默认落在 `out/`,该目录不进版本库。详情页逐页生成时,一页一个提示词文件,参考图顺序固定为产品主图在前、细节图在后。提示词含大量引号或换行时用 `-f` 传文件,避免命令行转义问题。

脚本只存在于仓库与镜像工作区,单独安装技能目录时不带 `tools/`。
17 changes: 16 additions & 1 deletion ecom-main-image-prompts/references/tool-adaptation.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
| GPT-Image(2 / 2.5 系) | 中文渲染较强,长文本准确率下降;文字用引号包裹、指定字体可提升 | 好,跨图商品一致性较好 | 分层结构:任务/主体/结构约束/光线材质/输出边界 | 无负词字段,用"输出边界"句 |
| Seedream(即梦 4/5 系) | 中文海报文字可直接用,生僻字偶有出错 | 支持十图级输入 | 官方建议自然语言连贯描述加短词美学,避免复杂堆词 | 无负词字段 |
| 可灵 Kling(图片 3 / O1 系) | 中文生态内表现好 | 支持主体一致性控制与多图参考 | 对话式、编辑式,适合同主体多轮迭代 | 自然语言句 |
| Qwen-Image 系 | 强化汉字渲染 | 支持 | 原生支持长指令(千 token 级),中文长 SOP 可直接投喂 | 自然语言句 |
| Qwen-Image 2.1 Pro / 3.0 系 | 强化汉字渲染,短标题稳定 | 2.1 系支持 1 到 10 张参考图;3.0 系 1 到 3 张 | 原生支持长指令(千 token 级),中文长 SOP 可直接投喂 | 2.1 系无负词字段,用自然语言边界句;3.0 系支持 negative_prompt |
| FLUX 系(本地/开源) | 文本渲染持续改善 | 支持多参考图 | 关键词与结构化描述均可 | 保留种子、负词、LoRA 等精细控制 |

多项为社区实测结论,工具版本迭代快,以官方为准。
Expand All @@ -22,6 +22,7 @@
- **Seedream(即梦)**:把结构化清单改写成连贯自然语言句子,保留短词审美描述(如"暖调柔光""哑光纸质"),删掉堆叠形容词。
- **GPT-Image**:按分层结构组织——任务、主体、结构约束、光线与材质、输出边界,逐层成段。
- **Gemini**:对话式写法——先给参考图与整体要求,图上要出现的文字用引号包裹,随后给多轮修图步骤(如需)。
- **Qwen-Image 2.1 Pro**:完整 SOP 原文可直接投喂。图生图时参考图按「产品主图在前、细节图在后」排序,并在提示词里说明每张图的职责;脚本默认不改写提示词,避免模型重写稀释 SOP 里的产品锁定条款。
- **FLUX 等本地模型**:可附种子与负面提示词列表,便于批量复现。

通用版写法(未指定工具时):主体描述在前(任务、产品、场景、构图),材质与光线居中,文字与边界收尾;一份提示词内保持同一套层级顺序。
Expand All @@ -46,3 +47,17 @@
```

规则:每步重申保留清单;一次只改一个变量;效果变差时回退上一步重新下达指令,不要叠加新指令硬改。

## 五、本仓库命令行出图通道(Qwen-Image 2.1 Pro)

仓库内 `tools/qwen_image.py` 提供文生图与图生图命令行通道,走阿里云百炼 DashScope 同步接口,模型 `qwen-image-2.1-pro`。密钥只从环境变量 `DASHSCOPE_API_KEY` 或 `--key-file` 读取,不写入任何文件。

```bash
export DASHSCOPE_API_KEY="sk-..." # 也可用 --key-file 指向密钥文件
python tools/qwen_image.py -p "提示词" -o out
python tools/qwen_image.py -f prompt.txt -i product_front.jpg -i product_detail.jpg --size 1024*1024
```

参数要点:`--size` 用 `宽*高`(也接受 `宽x高`)或 `auto`;`-n` 支持 1 到 6 张;`--seed` 固定随机种子;`-i` 可重复,最多 10 张参考图。提示词默认按原文投喂、不做模型改写,需要模型按简单描述扩写时加 `--prompt-extend`。输出默认落在 `out/`,该目录不进版本库。提示词含大量引号或换行时用 `-f` 传文件,避免命令行转义问题。

脚本只存在于仓库与镜像工作区,单独安装技能目录时不带 `tools/`。
205 changes: 205 additions & 0 deletions tools/qwen_image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
#!/usr/bin/env python3
"""Qwen-Image 2.1 Pro 出图通道(阿里云百炼 DashScope 同步接口)。

密钥只从环境变量 DASHSCOPE_API_KEY 或 --key-file 读取,不落盘、不打印。

用法:
python tools/qwen_image.py -p "提示词" -o out
python tools/qwen_image.py -f prompt.txt -i front.jpg -i detail.jpg --size 1024*1024
python tools/qwen_image.py --selftest
"""

import argparse
import base64
import json
import mimetypes
import os
import re
import sys
import time
import urllib.error
import urllib.request
from pathlib import Path

API_URL = "https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation"
DEFAULT_MODEL = "qwen-image-2.1-pro"
MAX_IMAGES = 10


def read_key(key_file):
if key_file:
key = Path(key_file).read_text(encoding="utf-8-sig").strip()
else:
key = os.environ.get("DASHSCOPE_API_KEY", "").strip()
if not key:
sys.exit("缺少 API key:设置环境变量 DASHSCOPE_API_KEY,或用 --key-file 指定文件")
return key


def normalize_size(size):
"""接受 1024*1024 / 1024x1024 / auto,输出 DashScope 要求的 宽*高。"""
if size is None or size == "auto":
return "auto"
m = re.fullmatch(r"(\d+)[*x×](\d+)", size.strip())
if not m:
sys.exit(f"size 格式错误:{size}(应为 auto 或 宽*高,如 1024*1024)")
w, h = int(m.group(1)), int(m.group(2))
if not (512 <= w <= 2048 and 512 <= h <= 2048):
sys.exit(f"size 超范围:{w}x{h}(边长 512 到 2048)")
if max(w, h) / min(w, h) > 8:
sys.exit(f"size 宽高比超范围:{w}:{h}(上限 8:1)")
return f"{w}*{h}"


def encode_image(path):
p = Path(path)
if not p.is_file():
sys.exit(f"参考图不存在:{path}")
if p.stat().st_size > 10 * 1024 * 1024:
sys.exit(f"参考图超过 10MB:{path}")
mime = mimetypes.guess_type(p.name)[0] or "image/png"
if not mime.startswith("image/"):
sys.exit(f"不支持的图片格式:{path}")
return f"data:{mime};base64,{base64.b64encode(p.read_bytes()).decode()}"


def build_payload(prompt, images, size, n, seed, prompt_extend, watermark, model):
content = [{"text": prompt}] + [{"image": img} for img in images]
params = {"size": normalize_size(size), "n": n, "prompt_extend": prompt_extend, "watermark": watermark}
if seed is not None:
params["seed"] = seed
return {"model": model, "input": {"messages": [{"role": "user", "content": content}]}, "parameters": params}


def parse_images(resp):
"""从 DashScope 响应里取出图片 URL;字段缺失时回退到递归找 image 键。"""
urls = []
for choice in resp.get("output", {}).get("choices", []):
for item in choice.get("message", {}).get("content", []):
if isinstance(item, dict) and item.get("image"):
urls.append(item["image"])
if not urls:
def walk(node):
if isinstance(node, dict):
for k, v in node.items():
if k == "image" and isinstance(v, str):
urls.append(v)
else:
walk(v)
elif isinstance(node, list):
for v in node:
walk(v)
walk(resp.get("output", {}))
return urls


def request(payload, key, timeout):
req = urllib.request.Request(
API_URL,
data=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=timeout) as r:
return json.loads(r.read().decode("utf-8"))
except urllib.error.HTTPError as e:
body = e.read().decode("utf-8", "replace")
sys.exit(f"HTTP {e.code}:{body[:800]}")
except urllib.error.URLError as e:
sys.exit(f"请求失败:{e.reason}")


def download(url, out_dir, index):
out_dir.mkdir(parents=True, exist_ok=True)
ext = Path(url.split("?")[0]).suffix.lower() or ".png"
path = out_dir / f"qwen_{time.strftime('%Y%m%d_%H%M%S')}_{index}{ext}"
with urllib.request.urlopen(url, timeout=120) as r, open(path, "wb") as f:
f.write(r.read())
return path


def selftest():
assert normalize_size("1024*1024") == "1024*1024"
assert normalize_size("1024x1536") == "1024*1536"
assert normalize_size("auto") == "auto"
for bad in ("511*1024", "4096*4096", "1024", "1024*2048*3"):
try:
normalize_size(bad)
except SystemExit:
pass
else:
raise AssertionError(f"应拒绝 {bad}")
p = build_payload("测试", ["data:image/png;base64,AA"], "1024*1024", 1, 7, False, False, DEFAULT_MODEL)
assert p["parameters"] == {"size": "1024*1024", "n": 1, "prompt_extend": False, "watermark": False, "seed": 7}
assert p["input"]["messages"][0]["content"][0] == {"text": "测试"}
assert p["input"]["messages"][0]["content"][1] == {"image": "data:image/png;base64,AA"}
# 默认必须关闭改写:长 SOP 原文投喂时不能被模型重写
defaults = {action.dest: action.default for action in build_parser()._actions}
assert defaults["prompt_extend"] is False, "prompt_extend 默认应为 False"
fake = {"output": {"choices": [{"message": {"content": [{"image": "https://a/1.png"}]}}]}}
assert parse_images(fake) == ["https://a/1.png"]
assert parse_images({"output": {"results": [{"url": "x", "image": "https://a/2.png"}]}}) == ["https://a/2.png"]
print("selftest ok")


def build_parser():
ap = argparse.ArgumentParser(description="Qwen-Image 2.1 Pro 出图")
ap.add_argument("-p", "--prompt", help="提示词文本")
ap.add_argument("-f", "--prompt-file", help="从文件读取提示词")
ap.add_argument("-i", "--image", action="append", default=[], help="参考图路径,可重复,最多 10 张")
ap.add_argument("--size", default="1024*1024", help="auto 或 宽*高,默认 1024*1024")
ap.add_argument("-n", "--num", type=int, default=1, help="出图数量 1 到 6,默认 1")
ap.add_argument("--seed", type=int, help="随机种子,固定后结果相对稳定")
ap.add_argument("--prompt-extend", action="store_true", help="开启提示词智能改写,默认关闭(SOP 原文直接投喂时保原样)")
ap.add_argument("--watermark", action="store_true", help="添加水印,默认不加")
ap.add_argument("--model", default=DEFAULT_MODEL, help=f"模型名,默认 {DEFAULT_MODEL}")
ap.add_argument("--key-file", help="API key 文件路径;不传则读 DASHSCOPE_API_KEY")
ap.add_argument("-o", "--out", default="out", help="输出目录,默认 out")
ap.add_argument("--timeout", type=int, default=300, help="请求超时秒数,默认 300")
ap.add_argument("--selftest", action="store_true", help="离线自检,不调用 API")
return ap


def main():
args = build_parser().parse_args()

if args.selftest:
selftest()
return

prompt = args.prompt
if args.prompt_file:
prompt = Path(args.prompt_file).read_text(encoding="utf-8-sig").strip()
if not prompt:
sys.exit("缺少提示词:用 -p 或 -f 提供")
if len(args.image) > MAX_IMAGES:
sys.exit(f"参考图最多 {MAX_IMAGES} 张,当前 {len(args.image)} 张")
if not 1 <= args.num <= 6:
sys.exit("--num 取值范围 1 到 6")

key = read_key(args.key_file)
payload = build_payload(
prompt,
[encode_image(p) for p in args.image],
args.size,
args.num,
args.seed,
args.prompt_extend,
args.watermark,
args.model,
)

started = time.time()
resp = request(payload, key, args.timeout)
urls = parse_images(resp)
if not urls:
sys.exit(f"响应中未找到图片:{json.dumps(resp, ensure_ascii=False)[:800]}")

for idx, url in enumerate(urls, 1):
print(download(url, Path(args.out), idx))
print(f"request_id={resp.get('request_id', '-')} 耗时={time.time() - started:.1f}s", file=sys.stderr)


if __name__ == "__main__":
main()
Loading