From 0482a60b810de63d6e8186c0c207742992a153f0 Mon Sep 17 00:00:00 2001 From: ADC1920 <99816220+ADC1920@users.noreply.github.com> Date: Mon, 26 Oct 2026 21:31:17 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(tools):=20=E6=96=B0=E5=A2=9E=20Qwen-Im?= =?UTF-8?q?age=202.1=20Pro=20=E5=91=BD=E4=BB=A4=E8=A1=8C=E5=87=BA=E5=9B=BE?= =?UTF-8?q?=E9=80=9A=E9=81=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 支持文生图与最多 10 张参考图的图生图,走 DashScope 同步接口。 密钥只从 DASHSCOPE_API_KEY 或 --key-file 读取,不写入任何文件。 提示词默认按原文投喂,需要模型扩写时加 --prompt-extend。 输出目录 out/ 加入 .gitignore。 --- .gitignore | 1 + tools/qwen_image.py | 205 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 206 insertions(+) create mode 100644 tools/qwen_image.py diff --git a/.gitignore b/.gitignore index 6dcc622..4f2a6ce 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ node_modules/ *.local __pycache__/ memory/ +out/ diff --git a/tools/qwen_image.py b/tools/qwen_image.py new file mode 100644 index 0000000..b8a91ac --- /dev/null +++ b/tools/qwen_image.py @@ -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() From baeca5a8b170bad5deb6b2d9ca414f68747dee2c Mon Sep 17 00:00:00 2001 From: ADC1920 <99816220+ADC1920@users.noreply.github.com> Date: Mon, 26 Oct 2026 21:31:20 +0800 Subject: [PATCH 2/2] =?UTF-8?q?docs:=20=E8=A1=A5=E5=85=85=E4=BA=91?= =?UTF-8?q?=E7=AB=AF=E5=87=BA=E5=9B=BE=E8=AF=B4=E6=98=8E=E4=B8=8E=20Qwen-I?= =?UTF-8?q?mage=20=E5=B7=A5=E5=85=B7=E9=80=82=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit README 新增云端出图一节与工具表条目。 主图与详情页两篇 tool-adaptation 更新 Qwen 能力行(按 2.1/3.0 版本区分参考图上限与负词支持), 并各增命令行出图通道说明。 --- README.md | 13 +++++++++++++ .../references/tool-adaptation.md | 17 ++++++++++++++++- .../references/tool-adaptation.md | 17 ++++++++++++++++- 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b9bff35..28825f6 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/ecom-detail-page-prompts/references/tool-adaptation.md b/ecom-detail-page-prompts/references/tool-adaptation.md index f63f4e9..c4b00c0 100644 --- a/ecom-detail-page-prompts/references/tool-adaptation.md +++ b/ecom-detail-page-prompts/references/tool-adaptation.md @@ -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 等精细控制 | 多项为社区实测结论,工具版本迭代快,以官方为准。 @@ -22,6 +22,7 @@ - **Seedream(即梦)**:把结构化清单改写成连贯自然语言句子,保留短词审美描述,删掉堆叠形容词。 - **GPT-Image**:按分层结构组织——任务、主体、结构约束、光线与材质、输出边界,逐层成段。 - **Gemini**:对话式写法——先给参考图与整体要求,图内文字用引号包裹,随后可给多轮修图步骤。 +- **Qwen-Image 2.1 Pro**:完整页面提示词可直接投喂。逐页生成时每页单独调用,参考图按「产品主图在前、细节图在后」排序;脚本默认不改写提示词,避免模型重写破坏版式与文字锁定描述。 - **FLUX 等本地模型**:可附种子与负面提示词列表,便于批量复现。 通用版写法(未指定工具时):先给产品与页面主题,再给构图与版式结构,然后背景、光影、装饰系统,最后主副标题与信息模块文字、输出边界收尾;一页提示词内保持同一套层级顺序。 @@ -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/`。 diff --git a/ecom-main-image-prompts/references/tool-adaptation.md b/ecom-main-image-prompts/references/tool-adaptation.md index 60d2e13..b2482da 100644 --- a/ecom-main-image-prompts/references/tool-adaptation.md +++ b/ecom-main-image-prompts/references/tool-adaptation.md @@ -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 等精细控制 | 多项为社区实测结论,工具版本迭代快,以官方为准。 @@ -22,6 +22,7 @@ - **Seedream(即梦)**:把结构化清单改写成连贯自然语言句子,保留短词审美描述(如"暖调柔光""哑光纸质"),删掉堆叠形容词。 - **GPT-Image**:按分层结构组织——任务、主体、结构约束、光线与材质、输出边界,逐层成段。 - **Gemini**:对话式写法——先给参考图与整体要求,图上要出现的文字用引号包裹,随后给多轮修图步骤(如需)。 +- **Qwen-Image 2.1 Pro**:完整 SOP 原文可直接投喂。图生图时参考图按「产品主图在前、细节图在后」排序,并在提示词里说明每张图的职责;脚本默认不改写提示词,避免模型重写稀释 SOP 里的产品锁定条款。 - **FLUX 等本地模型**:可附种子与负面提示词列表,便于批量复现。 通用版写法(未指定工具时):主体描述在前(任务、产品、场景、构图),材质与光线居中,文字与边界收尾;一份提示词内保持同一套层级顺序。 @@ -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/`。