#!/usr/bin/env python3
"""出图提示词诊断:审阅提示词、指出缺失要素、给出优化版本。
通过 API易 调用文本模型(默认 gpt-5.6-luna)。纯标准库,零依赖。
支持两种模式:
1) 出图前诊断 —— 只给提示词
2) 出图后复诊 —— 同时给提示词和实际出图(模型看图反推哪条要素没落实)
"""
import argparse
import base64
import json
import os
import sys
import urllib.error
import urllib.request
DEFAULT_MODEL = "gpt-5.6-luna"
BASE_URL = "https://api.apiyi.com/v1/chat/completions"
MAX_IMAGES = 4
# 各目标模型的额外提醒,只在 --target 指定时追加
TARGET_NOTES = {
"nano-banana": "目标模型是 Nano Banana(Gemini 系):自然语言长句友好,可以写成连贯段落而非关键词堆砌;"
"参考图最多 14 张;分辨率走 imageSize 参数(1K/2K/4K),宽高比走 aspectRatio。",
"gpt-image": "目标模型是 GPT-Image 系:指令遵循强、画面内文字渲染准确,可以放心指定要出现的文字内容;"
"参考图最多 16 张;只有官转 gpt-image-2 支持蒙版局部重绘;分辨率走 size 参数。",
"seedream": "目标模型是 Seedream:中文语境理解好;参考图最多 10 张(输入+输出不超过 15);"
"5.0 与 5.0-pro 可以在提示词里要求输出透明背景的 PNG。",
"flux": "目标模型是 FLUX:偏好结构清晰的描述;FLUX.2 pro/max/flex 参考图最多 8 张,Kontext 只有 1 张。",
"grok": "目标模型是 Grok Imagine:参考图只有走 /v1/images/edits 才生效,"
"传给 /v1/images/generations 会被静默丢弃且照常计费;参考图最多 4 张。",
}
SCENE_NOTES = {
"portrait": "这是人像。重点检查:是否指定了唯一主光的方向与软硬、焦段与光圈、是否要求了自然肤质"
"(毛孔、绒毛、油光)、是否禁用了磨皮美颜、主体是否被挪出画面正中。",
"product": "这是产品图/电商图。重点检查:背景是否被指定为中性可控、光位与补光是否写清、"
"投影方向、是否明确禁止出现品牌名和文字(否则模型会自行编造)、是否留出放文案的空白。",
"scene": "这是环境场景。重点检查:具体时间与天气、唯一主光来源、机位高度与焦段、"
"是否加入了磨损与杂物等真实痕迹、画面里的人是否被要求不正对镜头。",
"illustration": "这是插画/非写实。去 AI 味的那套写实手法要降权,改为检查:画风是否被具体指认"
"(媒材、笔触、年代、参考流派)、配色方案、线条与上色方式、构图与留白。",
}
SYSTEM = """你是出图提示词诊断专家,服务于通过 API 直接调用图片模型的开发者和设计师。
API 调用是单次原子调用,提示词原样进模型,没有任何网页版那样的自动改写兜底,所以提示词质量直接决定成功率。
## 诊断量表
先按「六要素」逐项判定 ok(写清楚了)/ weak(提到但含糊)/ missing(完全没写):
1 subject 主体:材质、颜色、数量、状态是否具体
2 environment 环境:背景是什么、虚实关系
3 light 光线:光源方向、软硬、有无补光——必须存在一个可指认的主光
4 lens 镜头与视角:焦段、光圈、机位高度、俯仰角
5 tone 色调与介质:白平衡倾向、饱和度、胶片或数码质感
6 composition 构图:主体在画面什么位置、留白在哪
## 必须报出的风险项
- 出现 8K / 超高清 / 超精细 / 杰作 / 大师作品 / 完美 这类空泛质量词:它们不提高分辨率,
反而把画面推向过锐过饱和的渲染感,是「AI 味」的主要来源,必须建议删除并换成具体的光、镜头、介质描述。
- 在提示词里写分辨率(4K/8K/高清):无效。分辨率只由 size / imageSize 之类的参数决定。
- 一句话里塞了多个互不相关的编辑动作:单次成功率会显著下降,建议拆成多轮。
- 用「这个」「红框里的东西」等指代而不点名具体物体:编辑类任务最常见的失败原因。
- 没有声明画面内文字:模型可能自行编造品牌名或文案,商用场景等于废片。要么写清要出现什么字,要么明确禁止出现文字。
- 涉及真人、名人、受版权保护的角色、未成年人、暴力或成人内容:会被上游审核拦截,需要提示改写。
## 优化原则
- 补齐缺失要素,不要靠堆砌形容词把提示词写长。
- 用户已经明确指定过的要素原样保留,不要擅自改写。
- 要写实质感就加具体的光位、焦段光圈、胶片或数码介质、以及主动添加的瑕疵(毛孔、碎发、磨损、水渍),
不要用「真实感」「高级感」这类抽象词。
- 不要输出负面提示词语法(多数图片模型不支持独立的 negative prompt 字段),把「不要什么」直接写进正文。
- optimized_prompt 与 changes 必须与用户原提示词使用同一种语言,中文提示词就全中文,不要中英混写。
## 输出
严格输出以下 JSON,不要加代码围栏,不要额外解释:
{
"score": 0-100 的整数,表示这条提示词单次出图的可用程度,
"verdict": "一句话总评,不超过 40 字",
"elements": {"subject":"ok|weak|missing","environment":"...","light":"...","lens":"...","tone":"...","composition":"..."},
"risks": ["每条一句话,说明问题和后果;没有风险则给空数组"],
"optimized_prompt": "优化后的完整提示词正文,可直接复制使用",
"changes": ["逐条说明改了什么、为什么"],
"suggested_params": {"size":"1K|2K|4K","aspect":"如 1:1 / 16:9","note":"参数上的建议,没有则空字符串"}
}"""
REVIEW_EXTRA = """
## 本次是出图后复诊
用户已经用下面这条提示词出了图,实际结果附在后面。请对照提示词逐条核对:
哪些要求落实了、哪些没落实、模型自作主张加了什么。
risks 里要明确写出「提示词的哪一句没有被执行」,optimized_prompt 要针对这些偏差重写,
而不是泛泛地补要素。"""
def load_api_key():
"""优先读环境变量;否则在脚本所在目录及其父目录找 .env。"""
key = os.environ.get("APIYI_API_KEY")
if key:
return key
here = os.path.dirname(os.path.abspath(__file__))
for d in (here, os.path.dirname(here)):
env_path = os.path.join(d, ".env")
if os.path.exists(env_path):
with open(env_path, encoding="utf-8") as f:
for line in f:
line = line.strip()
if line.startswith("APIYI_API_KEY") and "=" in line:
return line.split("=", 1)[1].strip().strip('"').strip("'")
return None
def image_data_url(path):
mime = "image/png" if path.lower().endswith(".png") else "image/jpeg"
with open(path, "rb") as f:
return f"data:{mime};base64," + base64.b64encode(f.read()).decode()
def build_messages(prompt, images, target, scene):
system = SYSTEM
if images:
system += REVIEW_EXTRA
extras = [TARGET_NOTES[target]] if target else []
if scene and scene != "auto":
extras.append(SCENE_NOTES[scene])
if extras:
system += "\n\n## 本次的额外约束\n\n" + "\n".join("- " + e for e in extras)
content = [{"type": "text", "text": "待诊断的提示词:\n\n" + prompt}]
for path in images:
content.append({"type": "image_url", "image_url": {"url": image_data_url(path)}})
return [{"role": "system", "content": system},
{"role": "user", "content": content}]
def diagnose(api_key, model, messages):
payload = json.dumps({
"model": model,
"messages": messages,
"response_format": {"type": "json_object"},
}).encode()
req = urllib.request.Request(
BASE_URL, data=payload, method="POST",
headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
)
try:
with urllib.request.urlopen(req, timeout=180) as r:
resp = json.loads(r.read())
except urllib.error.HTTPError as e:
raise RuntimeError(f"请求失败 HTTP {e.code}:{e.read().decode(errors='replace')[:500]}")
text = resp["choices"][0]["message"]["content"].strip()
if text.startswith("```"): # 防御:个别模型仍会套代码围栏
text = text.split("\n", 1)[1].rsplit("```", 1)[0]
try:
return json.loads(text)
except json.JSONDecodeError:
raise RuntimeError("模型未返回合法 JSON,原始输出:\n" + text[:800])
MARK = {"ok": "✅", "weak": "⚠️", "missing": "❌"}
LABEL = {"subject": "主体", "environment": "环境", "light": "光线",
"lens": "镜头", "tone": "色调", "composition": "构图"}
def render(r):
out = [f"【诊断】{r.get('score', '?')}/100 —— {r.get('verdict', '')}", ""]
els = r.get("elements", {})
out.append("六要素:" + " ".join(
f"{LABEL.get(k, k)}{MARK.get(v, '?')}" for k, v in els.items()))
risks = r.get("risks") or []
if risks:
out += ["", "风险项:"] + [f" · {x}" for x in risks]
else:
out += ["", "风险项:无"]
out += ["", "【优化后的提示词】", "", r.get("optimized_prompt", "")]
changes = r.get("changes") or []
if changes:
out += ["", "【改了什么】"] + [f" - {x}" for x in changes]
p = r.get("suggested_params") or {}
bits = []
if p.get("size"):
bits.append(f"size={p['size']}")
if p.get("aspect"):
bits.append(f"aspect={p['aspect']}")
line = " ".join(bits)
if p.get("note"):
line = (line + ";" if line else "") + p["note"]
if line:
out += ["", "【参数建议】" + line]
return "\n".join(out)
def main():
parser = argparse.ArgumentParser(description="出图提示词诊断与优化")
parser.add_argument("prompt", help="待诊断的提示词")
parser.add_argument("-i", "--image", action="append", default=[],
help=f"实际出图的路径,可重复(最多 {MAX_IMAGES} 张);传入即进入出图后复诊模式")
parser.add_argument("-t", "--target", choices=sorted(TARGET_NOTES),
help="目标图片模型,用于追加该系列特有的提醒")
parser.add_argument("-s", "--scene", choices=["auto"] + sorted(SCENE_NOTES), default="auto",
help="题材,默认 auto(不追加题材专项检查)")
parser.add_argument("--model", default=os.environ.get("APIYI_TEXT_MODEL", DEFAULT_MODEL),
help=f"诊断用的文本模型,默认 {DEFAULT_MODEL}")
parser.add_argument("--json", action="store_true", help="输出原始 JSON,便于程序消费")
args = parser.parse_args()
api_key = load_api_key()
if not api_key:
sys.exit("未找到 API Key:请在技能目录的 .env 写一行 APIYI_API_KEY=sk-xxx,或设置同名环境变量")
if len(args.image) > MAX_IMAGES:
sys.exit(f"最多 {MAX_IMAGES} 张出图,收到 {len(args.image)} 张")
for path in args.image:
if not os.path.exists(path):
sys.exit(f"图片不存在:{path}")
messages = build_messages(args.prompt, args.image, args.target, args.scene)
try:
result = diagnose(api_key, args.model, messages)
except RuntimeError as e:
sys.exit(str(e))
print(json.dumps(result, ensure_ascii=False, indent=2) if args.json else render(result))
if __name__ == "__main__":
main()