#!/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()