@yuhan1124/draw-prompt 0.4.10 → 0.4.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/SKILL.md +2 -2
- package/package.json +1 -1
- package/scripts/prompt_cli.py +380 -6
package/README.md
CHANGED
|
@@ -135,7 +135,7 @@ overlay --image out.png --spec spec.json --out final.png # 按 overlay spec
|
|
|
135
135
|
visual-check --image final.png --spec spec.json # 单图质量门:尺寸/画幅/亮度/对比度
|
|
136
136
|
edit-check --reference ref.png --output edited.png # 改图质量门:主体保留 + 有效变化
|
|
137
137
|
intent-check --request "原始需求" --prompt "生成prompt" # 检查是否新增/偏离用户未要求内容
|
|
138
|
-
visual-regress references/visual-cases.jsonl #
|
|
138
|
+
visual-regress references/visual-cases.jsonl # 开发仓库本地:多场景/多输出 prompt 回归,支持 expect_/forbid_ 断言
|
|
139
139
|
lint --prompt "…" [--asset-type poster] [--text 必显文字] # 生图转化硬约束检查
|
|
140
140
|
benchmark references/golden-cases.jsonl --runs 3 # 开发仓库本地:golden cases 稳定性基准
|
|
141
141
|
revise --sample-id last --reason text_error # 按失败分类修订 Prompt
|
|
@@ -174,7 +174,7 @@ status # 数据 + 下游通道健康
|
|
|
174
174
|
4. 只有当用户明确要求“文字必须绝对准确/可后处理”或出图反馈属于 `text_error` 时,才切到 `--strict-text` + `overlay` 两段式兜底。
|
|
175
175
|
5. 用 `visual-check` 验证成品图尺寸、画幅、亮度、对比度和基础细节。
|
|
176
176
|
6. 参考图改图用 `edit-check` 验证“主体保留 + 背景/目标确实变化”。
|
|
177
|
-
7. 模板或策略变动后,在开发仓库本地跑 `visual-regress references/visual-cases.jsonl
|
|
177
|
+
7. 模板或策略变动后,在开发仓库本地跑 `visual-regress references/visual-cases.jsonl`,确认多场景回归通过;它会真实编译 `convert`、`variants`、`series`、`adapt`、`compose` 等单图/多输出入口。用 `expect_count`、`expect_asset_type(s)`、`expect_aspect(s)`、`expect_required_text(_all)`、`forbid_required_text`、`expect_prompt_contains`、`forbid_prompt_contains` 把真实场景的产品意图固化成门禁。
|
|
178
178
|
|
|
179
179
|
这条链路的默认目标不是替 gpt-image-2 重做排版引擎,而是减少跑偏、遗漏和廉价风格;
|
|
180
180
|
两段式 overlay 只是文字极端稳定性兜底,不作为普通用户的默认体验。
|
package/SKILL.md
CHANGED
|
@@ -8,7 +8,7 @@ description: >-
|
|
|
8
8
|
画图的指令"、"优化我的出图 prompt"、"按我的风格生成 prompt",或在用 GPT Image 2 /
|
|
9
9
|
gpt-image-2 出图前需要一段精准提示词时,使用本 skill。
|
|
10
10
|
metadata:
|
|
11
|
-
version: 0.4.
|
|
11
|
+
version: 0.4.12
|
|
12
12
|
openclaw:
|
|
13
13
|
anyBins: ["uv", "python3"]
|
|
14
14
|
---
|
|
@@ -168,7 +168,7 @@ overlay --image out.png --spec spec.json --out final.png # 精确中文字/
|
|
|
168
168
|
visual-check --image final.png --spec spec.json # 单图质量门
|
|
169
169
|
edit-check --reference ref.png --output edited.png # 参考图改图质量门
|
|
170
170
|
intent-check --request "原始需求" --prompt "生成prompt" # 意图保真检查
|
|
171
|
-
visual-regress references/visual-cases.jsonl #
|
|
171
|
+
visual-regress references/visual-cases.jsonl # 开发仓库本地:多场景/多输出回归,支持 expect_/forbid_ 断言
|
|
172
172
|
lint --prompt "…" [--asset-type poster] [--text 必显文字] # 生图转化硬约束检查
|
|
173
173
|
benchmark references/golden-cases.jsonl --runs 3 # 开发仓库本地:golden cases 转化稳定性基准
|
|
174
174
|
revise --sample-id last --reason text_error # 按失败分类修订 Prompt
|
package/package.json
CHANGED
package/scripts/prompt_cli.py
CHANGED
|
@@ -138,7 +138,7 @@ def ensure_home() -> None:
|
|
|
138
138
|
|
|
139
139
|
|
|
140
140
|
SCHEMA_VERSION = 1
|
|
141
|
-
COMPILER_VERSION = "0.4.
|
|
141
|
+
COMPILER_VERSION = "0.4.12"
|
|
142
142
|
|
|
143
143
|
|
|
144
144
|
PACKAGED_SKILL_FILES = [
|
|
@@ -2793,7 +2793,7 @@ def extract_structural_labels(request: str, asset_type: str) -> list[str]:
|
|
|
2793
2793
|
value = re.split(stop_words, match.group(1), maxsplit=1, flags=re.IGNORECASE)[0]
|
|
2794
2794
|
value = re.sub(r"[()()]", "、", value)
|
|
2795
2795
|
value = re.sub(r"(?:进入|再到|到|输出|->|→)", "、", value)
|
|
2796
|
-
for part_match in re.finditer(r"[
|
|
2796
|
+
for part_match in re.finditer(r"[^、,,;;|]+", value):
|
|
2797
2797
|
part = part_match.group(0).strip(" \t\n\r,,、::")
|
|
2798
2798
|
part = re.sub(r"^(?:[^::]{0,12}(?:指标卡|卡片|列表|标题|模块|节点|步骤|栏目|分支))[::]", "", part)
|
|
2799
2799
|
part = re.sub(r"^(?:和|与|及|以及|and|再|则)\s*", "", part, flags=re.IGNORECASE).strip()
|
|
@@ -4867,9 +4867,23 @@ def cmd_edit_check(args: argparse.Namespace) -> int:
|
|
|
4867
4867
|
|
|
4868
4868
|
def visual_case_compile(case: dict) -> dict:
|
|
4869
4869
|
tool = str(case.get("tool") or case.get("cmd") or "convert")
|
|
4870
|
-
if tool in {"convert", "rewrite"
|
|
4870
|
+
if tool in {"convert", "rewrite"}:
|
|
4871
4871
|
compiled = compile_visual_case(case, target=case.get("target") or "codex-image")
|
|
4872
|
-
return {**compiled, "tool": tool}
|
|
4872
|
+
return with_compiled_items({**compiled, "tool": tool}, [{"id": case.get("id") or tool, **compiled}])
|
|
4873
|
+
if tool == "variants":
|
|
4874
|
+
return compile_visual_variants_case(case)
|
|
4875
|
+
if tool == "series":
|
|
4876
|
+
return compile_visual_series_case(case)
|
|
4877
|
+
if tool == "adapt":
|
|
4878
|
+
return compile_visual_adapt_case(case)
|
|
4879
|
+
if tool == "compose":
|
|
4880
|
+
return compile_visual_compose_case(case)
|
|
4881
|
+
if tool == "brand":
|
|
4882
|
+
return compile_visual_brand_case(case)
|
|
4883
|
+
if tool == "character":
|
|
4884
|
+
return compile_visual_character_case(case)
|
|
4885
|
+
if tool == "data-viz":
|
|
4886
|
+
return compile_visual_data_viz_case(case)
|
|
4873
4887
|
raise ValueError(f"visual-regress 暂不支持 tool={tool}")
|
|
4874
4888
|
|
|
4875
4889
|
|
|
@@ -4882,6 +4896,7 @@ def cmd_visual_regress(args: argparse.Namespace) -> int:
|
|
|
4882
4896
|
results = []
|
|
4883
4897
|
lint_errors = 0
|
|
4884
4898
|
intent_errors = 0
|
|
4899
|
+
expectation_errors = 0
|
|
4885
4900
|
visual_errors = 0
|
|
4886
4901
|
missing_images = 0
|
|
4887
4902
|
for idx, case in enumerate(cases, start=1):
|
|
@@ -4907,16 +4922,22 @@ def cmd_visual_regress(args: argparse.Namespace) -> int:
|
|
|
4907
4922
|
compiled = visual_case_compile(case)
|
|
4908
4923
|
lint = compiled["lint"]
|
|
4909
4924
|
intent = compiled.get("intent_check", [])
|
|
4925
|
+
expectation_findings = visual_case_expectation_findings(case, compiled)
|
|
4910
4926
|
lint_errors += sum(1 for item in lint if item.get("severity") == "error")
|
|
4911
4927
|
intent_errors += sum(1 for item in intent if item.get("severity") == "error")
|
|
4928
|
+
expectation_errors += sum(1 for item in expectation_findings if item.get("severity") == "error")
|
|
4912
4929
|
item = {
|
|
4913
4930
|
"id": case_id,
|
|
4914
4931
|
"scenario": case.get("scenario") or case.get("tool") or "convert",
|
|
4915
4932
|
"prompt_digest": compiled["prompt_digest"],
|
|
4916
4933
|
"asset_type": compiled["spec"]["asset_type"],
|
|
4917
4934
|
"aspect": compiled["spec"]["aspect"],
|
|
4935
|
+
"item_count": len(compiled.get("items") or [compiled]),
|
|
4936
|
+
"asset_types": [(entry.get("spec") or {}).get("asset_type") for entry in (compiled.get("items") or [compiled])],
|
|
4937
|
+
"aspects": [(entry.get("spec") or {}).get("aspect") for entry in (compiled.get("items") or [compiled])],
|
|
4918
4938
|
"lint": lint,
|
|
4919
4939
|
"intent_check": intent,
|
|
4940
|
+
"expectation_findings": expectation_findings,
|
|
4920
4941
|
"status": "compiled",
|
|
4921
4942
|
}
|
|
4922
4943
|
image = case.get("image") or case.get("output")
|
|
@@ -4951,15 +4972,20 @@ def cmd_visual_regress(args: argparse.Namespace) -> int:
|
|
|
4951
4972
|
"cases": len(cases),
|
|
4952
4973
|
"lint_error_count": lint_errors,
|
|
4953
4974
|
"intent_error_count": intent_errors,
|
|
4975
|
+
"expectation_error_count": expectation_errors,
|
|
4954
4976
|
"visual_error_count": visual_errors,
|
|
4955
4977
|
"missing_image_count": missing_images,
|
|
4956
|
-
"pass": lint_errors == 0 and intent_errors == 0 and visual_errors == 0 and missing_images == 0,
|
|
4978
|
+
"pass": lint_errors == 0 and intent_errors == 0 and expectation_errors == 0 and visual_errors == 0 and missing_images == 0,
|
|
4957
4979
|
}
|
|
4958
4980
|
output = {"summary": summary, "results": results}
|
|
4959
4981
|
if args.json:
|
|
4960
4982
|
print(json.dumps(output, ensure_ascii=False, indent=2))
|
|
4961
4983
|
else:
|
|
4962
|
-
print(
|
|
4984
|
+
print(
|
|
4985
|
+
f"visual-regress: cases={summary['cases']} pass={summary['pass']} "
|
|
4986
|
+
f"lint_errors={lint_errors} intent_errors={intent_errors} expectation_errors={expectation_errors} "
|
|
4987
|
+
f"visual_errors={visual_errors} missing_images={missing_images}"
|
|
4988
|
+
)
|
|
4963
4989
|
for item in results:
|
|
4964
4990
|
print(f"- {item['id']}: {item['status']} asset={item.get('asset_type', '?')} digest={item.get('prompt_digest', '?')}")
|
|
4965
4991
|
return 0 if summary["pass"] else 1
|
|
@@ -5025,6 +5051,354 @@ def compile_visual_case(
|
|
|
5025
5051
|
}
|
|
5026
5052
|
|
|
5027
5053
|
|
|
5054
|
+
def with_compiled_items(compiled: dict, items: list[dict]) -> dict:
|
|
5055
|
+
normalized = []
|
|
5056
|
+
for idx, item in enumerate(items, start=1):
|
|
5057
|
+
normalized.append(
|
|
5058
|
+
{
|
|
5059
|
+
"id": item.get("id") or f"item-{idx:02d}",
|
|
5060
|
+
"spec": item["spec"],
|
|
5061
|
+
"prompt": item["prompt"],
|
|
5062
|
+
"prompt_digest": item.get("prompt_digest") or prompt_digest(item["prompt"]),
|
|
5063
|
+
"lint": item.get("lint") or [],
|
|
5064
|
+
"intent_check": item.get("intent_check") or [],
|
|
5065
|
+
"handoff": item.get("handoff"),
|
|
5066
|
+
"text_overlay_spec": item.get("text_overlay_spec"),
|
|
5067
|
+
"acceptance_criteria": item.get("acceptance_criteria", []),
|
|
5068
|
+
}
|
|
5069
|
+
)
|
|
5070
|
+
if len(normalized) == 1:
|
|
5071
|
+
out = dict(compiled)
|
|
5072
|
+
out["items"] = normalized
|
|
5073
|
+
return out
|
|
5074
|
+
joined_digests = "|".join(item["prompt_digest"] for item in normalized)
|
|
5075
|
+
bundle = dict(compiled)
|
|
5076
|
+
bundle.update(
|
|
5077
|
+
{
|
|
5078
|
+
"spec": {
|
|
5079
|
+
"asset_type": "bundle",
|
|
5080
|
+
"aspect": "multi",
|
|
5081
|
+
"template_id": "multi_output",
|
|
5082
|
+
"required_text": [],
|
|
5083
|
+
"strict_text": False,
|
|
5084
|
+
},
|
|
5085
|
+
"prompt": "\n\n".join(item["prompt"] for item in normalized),
|
|
5086
|
+
"prompt_digest": prompt_digest(joined_digests),
|
|
5087
|
+
"lint": [finding for item in normalized for finding in item["lint"]],
|
|
5088
|
+
"intent_check": [finding for item in normalized for finding in item["intent_check"]],
|
|
5089
|
+
"handoff": None,
|
|
5090
|
+
"text_overlay_spec": None,
|
|
5091
|
+
"acceptance_criteria": [],
|
|
5092
|
+
"items": normalized,
|
|
5093
|
+
}
|
|
5094
|
+
)
|
|
5095
|
+
return bundle
|
|
5096
|
+
|
|
5097
|
+
|
|
5098
|
+
def visual_case_request(case: dict) -> str:
|
|
5099
|
+
request = case.get("request") or case.get("brief") or case.get("prompt_request") or case.get("input") or case.get("text")
|
|
5100
|
+
if isinstance(request, list):
|
|
5101
|
+
return "\n".join(str(item) for item in request)
|
|
5102
|
+
return str(request or "")
|
|
5103
|
+
|
|
5104
|
+
|
|
5105
|
+
def compile_visual_variants_case(case: dict) -> dict:
|
|
5106
|
+
request = visual_case_request(case)
|
|
5107
|
+
if not request:
|
|
5108
|
+
raise ValueError("variants case 缺少 request 字段")
|
|
5109
|
+
presets = parse_style_preset_list(str(case.get("style_presets") or ",".join(DEFAULT_VARIANT_PRESETS)), DEFAULT_VARIANT_PRESETS)
|
|
5110
|
+
custom_styles = case_list(case.get("custom_style") or case.get("custom_styles"))
|
|
5111
|
+
entries: list[dict] = [{"style_preset": preset, "style": case.get("shared_style")} for preset in presets]
|
|
5112
|
+
for custom in custom_styles:
|
|
5113
|
+
entries.append({"style_preset": "auto", "style": "; ".join([str(case.get("shared_style") or ""), custom]).strip("; "), "custom_style": custom})
|
|
5114
|
+
items = []
|
|
5115
|
+
for idx, entry in enumerate(entries, start=1):
|
|
5116
|
+
variant_case = dict(case)
|
|
5117
|
+
variant_case.update(
|
|
5118
|
+
{
|
|
5119
|
+
"request": request,
|
|
5120
|
+
"style": entry.get("style"),
|
|
5121
|
+
"style_preset": entry["style_preset"],
|
|
5122
|
+
"tags": "visual-regress,variants",
|
|
5123
|
+
}
|
|
5124
|
+
)
|
|
5125
|
+
variant_case.pop("tool", None)
|
|
5126
|
+
compiled = compile_visual_case(variant_case, target=case.get("target") or "codex-image", include_handoff=False)
|
|
5127
|
+
items.append({"id": f"variant-{idx:02d}", **compiled})
|
|
5128
|
+
return with_compiled_items({"tool": "variants", "count": len(items)}, items)
|
|
5129
|
+
|
|
5130
|
+
|
|
5131
|
+
def compile_visual_series_case(case: dict) -> dict:
|
|
5132
|
+
raw_items = case.get("briefs") or case.get("items") or case.get("series") or []
|
|
5133
|
+
if isinstance(raw_items, str):
|
|
5134
|
+
raw_items = [raw_items]
|
|
5135
|
+
if not raw_items:
|
|
5136
|
+
request = visual_case_request(case)
|
|
5137
|
+
if request:
|
|
5138
|
+
raw_items = [request]
|
|
5139
|
+
if not raw_items:
|
|
5140
|
+
raise ValueError("series case 缺少 briefs/items/request")
|
|
5141
|
+
shared_style = str(case.get("style") or case.get("shared_style") or "single coherent series style, same camera language, same palette discipline, same visual density")
|
|
5142
|
+
items = []
|
|
5143
|
+
for idx, item in enumerate(raw_items, start=1):
|
|
5144
|
+
item_case = dict(item) if isinstance(item, dict) else {"request": str(item)}
|
|
5145
|
+
brief = str(item_case.get("request") or item_case.get("brief") or "")
|
|
5146
|
+
if not brief:
|
|
5147
|
+
raise ValueError(f"series 第 {idx} 项缺少 request/brief")
|
|
5148
|
+
item_case["request"] = brief
|
|
5149
|
+
if case.get("asset_type") and not item_case.get("asset_type"):
|
|
5150
|
+
item_case["asset_type"] = case.get("asset_type")
|
|
5151
|
+
item_case["style"] = "; ".join([shared_style, str(item_case.get("style") or "").strip()]).strip("; ")
|
|
5152
|
+
for key in ("palette", "style_preset", "strict_text", "target"):
|
|
5153
|
+
if case.get(key) is not None and item_case.get(key) is None:
|
|
5154
|
+
item_case[key] = case.get(key)
|
|
5155
|
+
item_case["tags"] = "visual-regress,series"
|
|
5156
|
+
compiled = compile_visual_case(item_case, target=case.get("target") or "codex-image", include_handoff=False)
|
|
5157
|
+
items.append({"id": f"series-{idx:02d}", **compiled})
|
|
5158
|
+
return with_compiled_items({"tool": "series", "count": len(items)}, items)
|
|
5159
|
+
|
|
5160
|
+
|
|
5161
|
+
def compile_visual_adapt_case(case: dict) -> dict:
|
|
5162
|
+
request = visual_case_request(case)
|
|
5163
|
+
if not request:
|
|
5164
|
+
raise ValueError("adapt case 缺少 request 字段")
|
|
5165
|
+
aspects = split_csv(str(case.get("aspects") or case.get("aspect") or "1:1,3:4,16:9,9:16"))
|
|
5166
|
+
items = []
|
|
5167
|
+
for idx, aspect in enumerate(aspects, start=1):
|
|
5168
|
+
asset_type = str(case.get("asset_type") or route_asset_type(request))
|
|
5169
|
+
item_case = dict(case)
|
|
5170
|
+
item_case.update(
|
|
5171
|
+
{
|
|
5172
|
+
"request": request,
|
|
5173
|
+
"asset_type": asset_type,
|
|
5174
|
+
"aspect": aspect,
|
|
5175
|
+
"layout": case.get("layout") or adapt_layout_for_aspect(aspect, asset_type),
|
|
5176
|
+
"tags": "visual-regress,adapt",
|
|
5177
|
+
}
|
|
5178
|
+
)
|
|
5179
|
+
item_case.pop("tool", None)
|
|
5180
|
+
compiled = compile_visual_case(item_case, target=case.get("target") or "codex-image", include_handoff=False)
|
|
5181
|
+
items.append({"id": f"adapt-{idx:02d}", **compiled})
|
|
5182
|
+
return with_compiled_items({"tool": "adapt", "count": len(items)}, items)
|
|
5183
|
+
|
|
5184
|
+
|
|
5185
|
+
def compile_visual_compose_case(case: dict) -> dict:
|
|
5186
|
+
text = visual_case_request(case)
|
|
5187
|
+
if not text:
|
|
5188
|
+
raise ValueError("compose case 缺少 request/input 字段")
|
|
5189
|
+
max_images = int(case.get("max_images") or 6)
|
|
5190
|
+
shared_style = str(case.get("shared_style") or infer_compose_style(text))
|
|
5191
|
+
chunks = split_document_sections(text, max_images)
|
|
5192
|
+
items = []
|
|
5193
|
+
for idx, chunk in enumerate(chunks, start=1):
|
|
5194
|
+
asset_type = choose_compose_asset(chunk, idx - 1)
|
|
5195
|
+
labels = extract_visual_labels(chunk, asset_type)
|
|
5196
|
+
purpose = compose_purpose(asset_type, idx)
|
|
5197
|
+
item_case = {
|
|
5198
|
+
"request": f"{purpose}。根据这段内容生成对应画面:{chunk}",
|
|
5199
|
+
"asset_type": asset_type,
|
|
5200
|
+
"style": shared_style,
|
|
5201
|
+
"style_preset": case.get("style_preset"),
|
|
5202
|
+
"palette": case.get("palette"),
|
|
5203
|
+
"text": labels if (case.get("strict_text") or asset_type in {"diagram", "infographic", "ui"}) else [],
|
|
5204
|
+
"strict_text": bool(case.get("strict_text", False)),
|
|
5205
|
+
"target": case.get("target") or "codex-image",
|
|
5206
|
+
"tags": "visual-regress,compose",
|
|
5207
|
+
}
|
|
5208
|
+
compiled = compile_visual_case(item_case, target=case.get("target") or "codex-image", include_handoff=False)
|
|
5209
|
+
items.append({"id": f"compose-{idx:02d}", **compiled})
|
|
5210
|
+
return with_compiled_items({"tool": "compose", "count": len(items), "shared_style": shared_style}, items)
|
|
5211
|
+
|
|
5212
|
+
|
|
5213
|
+
def compile_visual_brand_case(case: dict) -> dict:
|
|
5214
|
+
name = str(case.get("name") or "")
|
|
5215
|
+
request = visual_case_request(case)
|
|
5216
|
+
if not name:
|
|
5217
|
+
raise ValueError("brand case 缺少 name 字段")
|
|
5218
|
+
brand_block = "\n".join(
|
|
5219
|
+
[
|
|
5220
|
+
f"Brand system for invented brand \"{name}\":",
|
|
5221
|
+
f"- Industry: {case.get('industry') or 'unspecified'}.",
|
|
5222
|
+
f"- Values: {', '.join(case_list(case.get('values'))) or 'clear, trustworthy, distinctive'}.",
|
|
5223
|
+
f"- Palette: {case.get('palette') or '2-3 controlled brand colors plus neutral support'}.",
|
|
5224
|
+
f"- Style: {case.get('style') or 'original, consistent, premium but restrained brand system'}.",
|
|
5225
|
+
"- Rules: keep mark shapes original; use the same spacing, palette, and typography logic across assets; no real brand logos.",
|
|
5226
|
+
]
|
|
5227
|
+
)
|
|
5228
|
+
item_case = dict(case)
|
|
5229
|
+
item_case.update({"request": f"{request}\n{brand_block}" if request else brand_block, "text": case.get("text") or [name], "tags": "visual-regress,brand"})
|
|
5230
|
+
item_case.pop("tool", None)
|
|
5231
|
+
compiled = compile_visual_case(item_case, target=case.get("target") or "codex-image", include_handoff=False)
|
|
5232
|
+
return with_compiled_items({"tool": "brand", **compiled}, [{"id": "brand-01", **compiled}])
|
|
5233
|
+
|
|
5234
|
+
|
|
5235
|
+
def compile_visual_character_case(case: dict) -> dict:
|
|
5236
|
+
name = str(case.get("name") or "")
|
|
5237
|
+
description = str(case.get("description") or visual_case_request(case))
|
|
5238
|
+
if not name or not description:
|
|
5239
|
+
raise ValueError("character case 缺少 name/description")
|
|
5240
|
+
identity = (
|
|
5241
|
+
f"Original character \"{name}\": {description}. "
|
|
5242
|
+
f"Outfit anchors: {case.get('outfit') or 'stable signature outfit and silhouette'}. "
|
|
5243
|
+
f"Palette: {case.get('palette') or 'stable limited palette'}. Keep the same identity anchors in every image; no existing IP resemblance."
|
|
5244
|
+
)
|
|
5245
|
+
items = []
|
|
5246
|
+
reference_case = {"request": f"角色设定三视图和表情板:{identity}", "asset_type": "character", "style": case.get("style"), "style_preset": case.get("style_preset"), "text": [name], "tags": "visual-regress,character"}
|
|
5247
|
+
reference = compile_visual_case(reference_case, target=case.get("target") or "codex-image", include_handoff=False)
|
|
5248
|
+
items.append({"id": "character-reference", **reference})
|
|
5249
|
+
for idx, scene in enumerate(case_list(case.get("scene") or case.get("scenes")), start=1):
|
|
5250
|
+
scene_case = {"request": f"{identity} 场景图:{scene}", "asset_type": "illustration", "style": case.get("style"), "style_preset": case.get("style_preset"), "tags": "visual-regress,character-scene"}
|
|
5251
|
+
compiled = compile_visual_case(scene_case, target=case.get("target") or "codex-image", include_handoff=False)
|
|
5252
|
+
items.append({"id": f"character-scene-{idx:02d}", **compiled})
|
|
5253
|
+
return with_compiled_items({"tool": "character", "count": len(items)}, items)
|
|
5254
|
+
|
|
5255
|
+
|
|
5256
|
+
def compile_visual_data_viz_case(case: dict) -> dict:
|
|
5257
|
+
request = str(case.get("request") or "根据数据生成清晰的信息图")
|
|
5258
|
+
data_preview = read_data_preview(str(case.get("file") or "")) if case.get("file") else {"columns": [], "rows": [], "row_count": 0, "source": ""}
|
|
5259
|
+
title = infer_chart_title(request, str(case.get("title") or "") or None)
|
|
5260
|
+
chart_type = infer_chart_type(request, data_preview["columns"], str(case.get("chart_type") or "") or None)
|
|
5261
|
+
required_text = [] if case.get("strict_text") else [title]
|
|
5262
|
+
prompt = "\n".join(
|
|
5263
|
+
[
|
|
5264
|
+
f"Create a 16:9 high-quality data visualization infographic for: {request}.",
|
|
5265
|
+
f"Chart type: {chart_type}.",
|
|
5266
|
+
f"Data schema: columns={', '.join(data_preview['columns']) or 'not provided'}, rows={data_preview['row_count']}.",
|
|
5267
|
+
f"Rows preview for visual truthfulness: {json.dumps(data_preview['rows'], ensure_ascii=False)}.",
|
|
5268
|
+
"Layout: title band, main chart area, short insight callouts, compact legend, and optional source note.",
|
|
5269
|
+
"Use honest scales, aligned axes, restrained colors, and no decorative fake data.",
|
|
5270
|
+
f'The title must read exactly "{title}". Keep labels large and readable.' if required_text else "Strict chart text mode: reserve clean title, axis, legend, and callout zones; exact labels and numbers will be applied as deterministic vector/text overlay.",
|
|
5271
|
+
"Avoid: misleading charts; unreadable microtext; random numbers not present in the data; fake logos; visual clutter.",
|
|
5272
|
+
]
|
|
5273
|
+
)
|
|
5274
|
+
lint = lint_prompt(prompt, "infographic", "high", required_text)
|
|
5275
|
+
spec = {
|
|
5276
|
+
"asset_type": "infographic",
|
|
5277
|
+
"aspect": "16:9",
|
|
5278
|
+
"template_id": "data_viz",
|
|
5279
|
+
"required_text": required_text,
|
|
5280
|
+
"strict_text": bool(case.get("strict_text")),
|
|
5281
|
+
"quality": "high",
|
|
5282
|
+
}
|
|
5283
|
+
compiled = {
|
|
5284
|
+
"spec": spec,
|
|
5285
|
+
"prompt": prompt,
|
|
5286
|
+
"prompt_digest": prompt_digest(prompt),
|
|
5287
|
+
"lint": lint,
|
|
5288
|
+
"intent_check": [],
|
|
5289
|
+
"handoff": None,
|
|
5290
|
+
"text_overlay_spec": None,
|
|
5291
|
+
"acceptance_criteria": [],
|
|
5292
|
+
}
|
|
5293
|
+
return with_compiled_items({"tool": "data-viz", **compiled}, [{"id": "data-viz-01", **compiled}])
|
|
5294
|
+
|
|
5295
|
+
|
|
5296
|
+
def case_list(value: object) -> list[str]:
|
|
5297
|
+
if value is None:
|
|
5298
|
+
return []
|
|
5299
|
+
if isinstance(value, list):
|
|
5300
|
+
return [str(item) for item in value if str(item).strip()]
|
|
5301
|
+
return [str(value)] if str(value).strip() else []
|
|
5302
|
+
|
|
5303
|
+
|
|
5304
|
+
def visual_case_expectation_findings(case: dict, compiled: dict) -> list[dict]:
|
|
5305
|
+
items = compiled.get("items") or [compiled]
|
|
5306
|
+
specs = [(item.get("spec") or {}) for item in items]
|
|
5307
|
+
prompts = [str(item.get("prompt") or "") for item in items]
|
|
5308
|
+
spec = specs[0] if len(specs) == 1 else (compiled.get("spec") or {})
|
|
5309
|
+
prompt = "\n".join(prompts)
|
|
5310
|
+
findings: list[dict] = []
|
|
5311
|
+
|
|
5312
|
+
expected_count = case.get("expect_count")
|
|
5313
|
+
if expected_count is not None and len(items) != int(expected_count):
|
|
5314
|
+
findings.append(
|
|
5315
|
+
{
|
|
5316
|
+
"severity": "error",
|
|
5317
|
+
"rule": "case.count_mismatch",
|
|
5318
|
+
"message": f"期望输出数量={expected_count},实际={len(items)}",
|
|
5319
|
+
}
|
|
5320
|
+
)
|
|
5321
|
+
|
|
5322
|
+
expected_asset = case.get("expect_asset_type")
|
|
5323
|
+
if expected_asset and len(items) == 1 and spec.get("asset_type") != expected_asset:
|
|
5324
|
+
findings.append(
|
|
5325
|
+
{
|
|
5326
|
+
"severity": "error",
|
|
5327
|
+
"rule": "case.asset_type_mismatch",
|
|
5328
|
+
"message": f"期望 asset_type={expected_asset},实际={spec.get('asset_type')}",
|
|
5329
|
+
}
|
|
5330
|
+
)
|
|
5331
|
+
expected_asset_types = case_list(case.get("expect_asset_types"))
|
|
5332
|
+
if expected_asset_types:
|
|
5333
|
+
actual = [str(item.get("asset_type") or "") for item in specs]
|
|
5334
|
+
if actual != expected_asset_types:
|
|
5335
|
+
findings.append(
|
|
5336
|
+
{
|
|
5337
|
+
"severity": "error",
|
|
5338
|
+
"rule": "case.asset_types_mismatch",
|
|
5339
|
+
"message": f"期望 asset_types={expected_asset_types},实际={actual}",
|
|
5340
|
+
}
|
|
5341
|
+
)
|
|
5342
|
+
|
|
5343
|
+
expected_aspect = case.get("expect_aspect")
|
|
5344
|
+
if expected_aspect and len(items) == 1 and spec.get("aspect") != expected_aspect:
|
|
5345
|
+
findings.append(
|
|
5346
|
+
{
|
|
5347
|
+
"severity": "error",
|
|
5348
|
+
"rule": "case.aspect_mismatch",
|
|
5349
|
+
"message": f"期望 aspect={expected_aspect},实际={spec.get('aspect')}",
|
|
5350
|
+
}
|
|
5351
|
+
)
|
|
5352
|
+
expected_aspects = case_list(case.get("expect_aspects"))
|
|
5353
|
+
if expected_aspects:
|
|
5354
|
+
actual = [str(item.get("aspect") or "") for item in specs]
|
|
5355
|
+
if actual != expected_aspects:
|
|
5356
|
+
findings.append(
|
|
5357
|
+
{
|
|
5358
|
+
"severity": "error",
|
|
5359
|
+
"rule": "case.aspects_mismatch",
|
|
5360
|
+
"message": f"期望 aspects={expected_aspects},实际={actual}",
|
|
5361
|
+
}
|
|
5362
|
+
)
|
|
5363
|
+
|
|
5364
|
+
expected_template = case.get("expect_template_id")
|
|
5365
|
+
if expected_template and spec.get("template_id") != expected_template:
|
|
5366
|
+
findings.append(
|
|
5367
|
+
{
|
|
5368
|
+
"severity": "error",
|
|
5369
|
+
"rule": "case.template_mismatch",
|
|
5370
|
+
"message": f"期望 template_id={expected_template},实际={spec.get('template_id')}",
|
|
5371
|
+
}
|
|
5372
|
+
)
|
|
5373
|
+
|
|
5374
|
+
labels_by_item = [set(item.get("required_text") or []) for item in specs]
|
|
5375
|
+
labels = set().union(*labels_by_item) if labels_by_item else set()
|
|
5376
|
+
for text in case_list(case.get("expect_required_text")):
|
|
5377
|
+
if text not in labels:
|
|
5378
|
+
findings.append({"severity": "error", "rule": "case.required_text_missing", "message": f"缺少必显文字:{text}"})
|
|
5379
|
+
elif f'"{text}"' not in prompt and not any(item.get("strict_text") for item in specs):
|
|
5380
|
+
findings.append({"severity": "error", "rule": "case.required_text_not_quoted", "message": f"Prompt 未逐字引用:{text}"})
|
|
5381
|
+
|
|
5382
|
+
for text in case_list(case.get("expect_required_text_all")):
|
|
5383
|
+
for idx, item_labels in enumerate(labels_by_item, start=1):
|
|
5384
|
+
if text not in item_labels:
|
|
5385
|
+
findings.append({"severity": "error", "rule": "case.required_text_all_missing", "message": f"第 {idx} 个输出缺少必显文字:{text}"})
|
|
5386
|
+
|
|
5387
|
+
for text in case_list(case.get("forbid_required_text")):
|
|
5388
|
+
if any(text in item_labels for item_labels in labels_by_item):
|
|
5389
|
+
findings.append({"severity": "error", "rule": "case.required_text_forbidden", "message": f"不应作为必显文字:{text}"})
|
|
5390
|
+
|
|
5391
|
+
for text in case_list(case.get("expect_prompt_contains")):
|
|
5392
|
+
if text not in prompt:
|
|
5393
|
+
findings.append({"severity": "error", "rule": "case.prompt_missing", "message": f"Prompt 缺少片段:{text}"})
|
|
5394
|
+
|
|
5395
|
+
for text in case_list(case.get("forbid_prompt_contains")):
|
|
5396
|
+
if text in prompt:
|
|
5397
|
+
findings.append({"severity": "error", "rule": "case.prompt_forbidden", "message": f"Prompt 不应包含片段:{text}"})
|
|
5398
|
+
|
|
5399
|
+
return findings
|
|
5400
|
+
|
|
5401
|
+
|
|
5028
5402
|
def has_lint_error(items: list[dict]) -> bool:
|
|
5029
5403
|
return any(item.get("severity") == "error" for item in items)
|
|
5030
5404
|
|