@yuhan1124/draw-prompt 0.4.12 → 0.4.13
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 +1 -1
- package/SKILL.md +1 -1
- package/package.json +1 -1
- package/references/conversion-skill-plan.md +1 -1
- package/scripts/prompt_cli.py +102 -12
package/README.md
CHANGED
|
@@ -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`,确认多场景回归通过;它会真实编译 `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` 把真实场景的产品意图固化成门禁。
|
|
177
|
+
7. 模板或策略变动后,在开发仓库本地跑 `visual-regress references/visual-cases.jsonl`,确认多场景回归通过;它会真实编译 `convert`、`rewrite`、`edit`、`variants`、`series`、`adapt`、`compose`、`brand`、`character`、`data-viz` 等单图/多输出入口。用 `expect_count`、`expect_asset_type(s)`、`expect_aspect(s)`、`expect_required_text(_all)`、`forbid_required_text`、`expect_safety_rewrite`、`expect_prompt_contains`、`forbid_prompt_contains` 把真实场景的产品意图固化成门禁。
|
|
178
178
|
|
|
179
179
|
这条链路的默认目标不是替 gpt-image-2 重做排版引擎,而是减少跑偏、遗漏和廉价风格;
|
|
180
180
|
两段式 overlay 只是文字极端稳定性兜底,不作为普通用户的默认体验。
|
package/SKILL.md
CHANGED
package/package.json
CHANGED
|
@@ -369,7 +369,7 @@ prompt,再交给下游出图。
|
|
|
369
369
|
| `ui_dashboard` | ui | 侧边栏、顶部栏、KPI、图表、表格 |
|
|
370
370
|
| `slide_corporate_report` | slide | 保留用户显式区域、栏目/行列结构、图标位置、页脚和留白 |
|
|
371
371
|
| `diagram_rag` | diagram | User -> Retriever -> Vector DB -> LLM -> Answer |
|
|
372
|
-
| `diagram_system` | diagram |
|
|
372
|
+
| `diagram_system` | diagram | 分层盒子、有向箭头、只保留用户要求的标签 |
|
|
373
373
|
|
|
374
374
|
## 质量门
|
|
375
375
|
|
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.13"
|
|
142
142
|
|
|
143
143
|
|
|
144
144
|
PACKAGED_SKILL_FILES = [
|
|
@@ -2320,7 +2320,7 @@ TEMPLATE_DEFS = {
|
|
|
2320
2320
|
"diagram_system": {
|
|
2321
2321
|
"asset_type": "diagram",
|
|
2322
2322
|
"label": "系统架构图",
|
|
2323
|
-
"layout": "layered system boxes with directional arrows
|
|
2323
|
+
"layout": "layered system boxes with directional arrows",
|
|
2324
2324
|
"keywords": ["架构", "系统", "模块", "服务", "流程"],
|
|
2325
2325
|
},
|
|
2326
2326
|
"product_hero": {
|
|
@@ -2593,6 +2593,9 @@ NON_DISPLAY_TEXTS = {
|
|
|
2593
2593
|
"关系",
|
|
2594
2594
|
"标签",
|
|
2595
2595
|
"卖点标签",
|
|
2596
|
+
"主视觉",
|
|
2597
|
+
"产品主视觉",
|
|
2598
|
+
"核心",
|
|
2596
2599
|
"购买转化氛围",
|
|
2597
2600
|
"但不要过度促销",
|
|
2598
2601
|
}
|
|
@@ -2642,12 +2645,13 @@ def extract_required_texts(request: str, explicit_texts: list[str]) -> list[str]
|
|
|
2642
2645
|
|
|
2643
2646
|
def clean_text(text: str) -> str:
|
|
2644
2647
|
text = text.strip(" \t\n\r,,、。;;::.!?!?")
|
|
2645
|
-
text = re.sub(r"^(?:[^::]{0,
|
|
2648
|
+
text = re.sub(r"^(?:[^::]{0,20}(?:活动路径|指标卡|卡片|列表|标题|模块|节点|步骤|栏目|分支|路径|卡))[::]", "", text)
|
|
2646
2649
|
text = re.sub(r"^(?:顶部|底部|中间|左侧|右侧|上方|下方|首页|页面)?(?:主标题|副标题|标题)\s+", "", text)
|
|
2647
2650
|
text = re.sub(r"^(?:问候语|核心卡片|主要按钮|主按钮|按钮)\s+", "", text)
|
|
2648
2651
|
text = re.sub(r"^(?:是|为|叫)\s+", "", text)
|
|
2649
2652
|
text = re.sub(r"^(?:一个|一枚|一项)?(?:明显的|醒目的|主要的|primary\s+)?(.{1,16})按钮$", r"\1", text, flags=re.IGNORECASE)
|
|
2650
|
-
text = re.sub(r"(
|
|
2653
|
+
text = re.sub(r"(?:[一二三四五六七八九十\d]+个)?(?:网格|列表|区域|模块|卡片)$", "", text)
|
|
2654
|
+
text = re.sub(r"\s*[一二三四五六七八九十\d]+个$", "", text)
|
|
2651
2655
|
text = re.sub(r"(?:要)?(?:渲染)?(?:清晰|清楚|可读|明显)$", "", text)
|
|
2652
2656
|
text = re.sub(r"(?:这些)?元素$", "", text)
|
|
2653
2657
|
text = re.sub(r"(?:[一二三四五六七八九十\d]+个)?步骤$", "", text)
|
|
@@ -2748,12 +2752,13 @@ def merge_texts(primary: list[str], extra: list[str]) -> list[str]:
|
|
|
2748
2752
|
seen: set[str] = set()
|
|
2749
2753
|
for item in primary + extra:
|
|
2750
2754
|
text = item.strip(" \t\n\r,,、。;;::.!?!?")
|
|
2751
|
-
text = re.sub(r"^(?:[^::]{0,
|
|
2755
|
+
text = re.sub(r"^(?:[^::]{0,20}(?:活动路径|指标卡|卡片|列表|标题|模块|节点|步骤|栏目|分支|路径|卡))[::]", "", text)
|
|
2752
2756
|
text = re.sub(r"^(?:顶部|底部|中间|左侧|右侧|上方|下方|首页|页面)?(?:主标题|副标题|标题)\s+", "", text)
|
|
2753
2757
|
text = re.sub(r"^(?:问候语|核心卡片|主要按钮|主按钮|按钮)\s+", "", text)
|
|
2754
2758
|
text = re.sub(r"^(?:是|为|叫)\s+", "", text)
|
|
2755
2759
|
text = re.sub(r"^(?:一个|一枚|一项)?(?:明显的|醒目的|主要的|primary\s+)?(.{1,16})按钮$", r"\1", text, flags=re.IGNORECASE)
|
|
2756
|
-
text = re.sub(r"(
|
|
2760
|
+
text = re.sub(r"(?:[一二三四五六七八九十\d]+个)?(?:网格|列表|区域|模块|卡片)$", "", text)
|
|
2761
|
+
text = re.sub(r"\s*[一二三四五六七八九十\d]+个$", "", text)
|
|
2757
2762
|
text = re.sub(r"(?:要)?(?:渲染)?(?:清晰|清楚|可读|明显)$", "", text)
|
|
2758
2763
|
text = re.sub(r"(?:这些)?元素$", "", text)
|
|
2759
2764
|
text = re.sub(r"(?:[一二三四五六七八九十\d]+个)?步骤$", "", text)
|
|
@@ -2768,19 +2773,30 @@ def merge_texts(primary: list[str], extra: list[str]) -> list[str]:
|
|
|
2768
2773
|
if text and key not in seen:
|
|
2769
2774
|
seen.add(key)
|
|
2770
2775
|
merged.append(text)
|
|
2771
|
-
|
|
2776
|
+
pruned: list[str] = []
|
|
2777
|
+
compact_items = [(text, re.sub(r"[\s,,、。;;::.!?!?]+", "", text)) for text in merged]
|
|
2778
|
+
for idx, (text, key) in enumerate(compact_items):
|
|
2779
|
+
if len(key) <= 8 and any(
|
|
2780
|
+
idx != other_idx and key and key in other_key and len(other_key) > len(key)
|
|
2781
|
+
for other_idx, (_, other_key) in enumerate(compact_items)
|
|
2782
|
+
):
|
|
2783
|
+
continue
|
|
2784
|
+
pruned.append(text)
|
|
2785
|
+
return pruned
|
|
2772
2786
|
|
|
2773
2787
|
|
|
2774
2788
|
def extract_structural_labels(request: str, asset_type: str) -> list[str]:
|
|
2775
2789
|
if asset_type not in {"diagram", "infographic", "slide", "ui", "poster", "product"}:
|
|
2776
2790
|
return []
|
|
2777
2791
|
candidates: list[tuple[int, str]] = []
|
|
2778
|
-
list_intro = r"(?:需要)?(
|
|
2792
|
+
list_intro = r"(?:需要)?(?:展示|呈现|列出|包含|包括|含有|分为|覆盖)"
|
|
2779
2793
|
stop_words = r"(?:\b16\s*:\s*9\b|\b9\s*:\s*16\b|\b3\s*:\s*4\b|\b1\s*:\s*1\b|适合|用于|画幅|aspect|高质量|高清|clean|corporate)"
|
|
2780
2794
|
patterns = [
|
|
2781
2795
|
rf"(?:结构(?:从左到右|从右到左|从上到下|从下到上)?|链路|流程)\s*[::]\s*([^。\n]{{2,240}})",
|
|
2782
2796
|
rf"(?:数据|指标|核心数据|关键指标|卖点|亮点|功能点|特性|活动路径|路径)\s*(?:包括|包含|有|为)?[::\s]*([^。;;\n]{{2,180}})",
|
|
2783
2797
|
rf"{list_intro}\s*(?:这些|以下|对应的)?(?:模块|部分|层|栏目|节点|入口|能力|场景|列表|指标卡|卡片|步骤|分支|数据|指标|卖点|亮点|功能点|特性|路径)?[::\s]*([^。;;\n]{{2,180}})",
|
|
2798
|
+
rf"(?:顶部|底部|中间|左侧|右侧|上方|下方|最终|末端)?\s*输出\s*(?!可交给|为|成|到|给)([^。;;\n]{{2,180}})",
|
|
2799
|
+
rf"(?:顶部|底部|中间|左侧|右侧|上方|下方|主区域|页面|首页)?\s*(?:有|显示|放置|排列|提供)\s*([^。;;\n]{{2,180}}?(?:[一二三四五六七八九十\d]+个)?(?:模块|卡片|按钮|入口|标签|列表))",
|
|
2784
2800
|
rf"(?:模块|节点|栏目|部分|层|入口|能力|场景|列表|指标卡|卡片|步骤|分支|数据|指标|卖点|亮点|功能点|特性|路径)\s*(?:包括|包含|有|为)[::\s]*([^。;;\n]{{2,180}})",
|
|
2785
2801
|
rf"(?:模块|节点|栏目|部分|层|入口|能力|场景|列表|指标卡|卡片|步骤|分支|数据|指标|卖点|亮点|功能点|特性|路径)[^。;;\n::]{{0,16}}[::]([^。;;\n]{{2,180}})",
|
|
2786
2802
|
]
|
|
@@ -2790,17 +2806,23 @@ def extract_structural_labels(request: str, asset_type: str) -> list[str]:
|
|
|
2790
2806
|
)
|
|
2791
2807
|
for pattern in patterns:
|
|
2792
2808
|
for match in re.finditer(pattern, request, flags=re.IGNORECASE):
|
|
2809
|
+
if re.search(r"\bicon\s*:|图标", match.group(0), flags=re.IGNORECASE):
|
|
2810
|
+
continue
|
|
2793
2811
|
value = re.split(stop_words, match.group(1), maxsplit=1, flags=re.IGNORECASE)[0]
|
|
2794
2812
|
value = re.sub(r"[()()]", "、", value)
|
|
2795
2813
|
value = re.sub(r"(?:进入|再到|到|输出|->|→)", "、", value)
|
|
2796
2814
|
for part_match in re.finditer(r"[^、,,;;|]+", value):
|
|
2797
2815
|
part = part_match.group(0).strip(" \t\n\r,,、::")
|
|
2798
|
-
part = re.sub(r"^(?:[^::]{0,
|
|
2816
|
+
part = re.sub(r"^(?:[^::]{0,20}(?:活动路径|指标卡|卡片|列表|标题|模块|节点|步骤|栏目|分支|路径|卡))[::]", "", part)
|
|
2799
2817
|
part = re.sub(r"^(?:和|与|及|以及|and|再|则)\s*", "", part, flags=re.IGNORECASE).strip()
|
|
2800
2818
|
part = re.sub(r"^(?:底部有|顶部有|左侧有|右侧有|上方有|下方有)\s*", "", part)
|
|
2801
2819
|
part = re.sub(r"^(?:包含|包括|含有)\s*", "", part)
|
|
2820
|
+
part = re.sub(r"^(?:问候语|核心卡片|主要按钮|主按钮|按钮)\s+", "", part)
|
|
2821
|
+
part = re.sub(r"^(?:一个|一枚|一项)?(?:明显的|醒目的|主要的|primary\s+)?(.{1,16})按钮$", r"\1", part, flags=re.IGNORECASE)
|
|
2802
2822
|
part = re.sub(r"\s*(?:和|与|及|以及|and)$", "", part, flags=re.IGNORECASE).strip()
|
|
2803
2823
|
part = re.sub(r"(?:这些)?元素$", "", part)
|
|
2824
|
+
part = re.sub(r"(?:[一二三四五六七八九十\d]+个)?(?:网格|列表|区域|模块|卡片)$", "", part)
|
|
2825
|
+
part = re.sub(r"\s*[一二三四五六七八九十\d]+个$", "", part)
|
|
2804
2826
|
part = re.sub(r"(?:[一二三四五六七八九十\d]+个)?步骤$", "", part)
|
|
2805
2827
|
part = part.strip(" \t\n\r,,、::")
|
|
2806
2828
|
if not part:
|
|
@@ -2854,7 +2876,7 @@ def infer_palette_constraints(request: str) -> list[str]:
|
|
|
2854
2876
|
]
|
|
2855
2877
|
constraints: list[str] = []
|
|
2856
2878
|
seen: set[str] = set()
|
|
2857
|
-
for sentence in re.split(r"[。;;\n]", request):
|
|
2879
|
+
for sentence in re.split(r"[。;;\n]|(?<=[A-Za-z)])\.\s+", request):
|
|
2858
2880
|
for fragment in re.split(r"[,,]", sentence):
|
|
2859
2881
|
fragment = fragment.strip(" \t\r\n。.;;,,")
|
|
2860
2882
|
if not fragment:
|
|
@@ -3112,7 +3134,7 @@ def infer_must_include(asset_type: str, template_id: str, texts: list[str], stri
|
|
|
3112
3134
|
"ui": ["requested screen type", "requested UI sections", "clear component hierarchy"],
|
|
3113
3135
|
"infographic": ["requested information units", "requested relationships", "clear visual hierarchy"],
|
|
3114
3136
|
"slide": ["widescreen slide canvas", "all requested sections", "requested visual motifs", "crisp readable text hierarchy"],
|
|
3115
|
-
"diagram": ["labeled components", "directional arrows", "
|
|
3137
|
+
"diagram": ["labeled components", "directional arrows", "clear flow semantics"],
|
|
3116
3138
|
"product": ["single hero product", "visible material texture", "controlled studio lighting"],
|
|
3117
3139
|
"photography": ["realistic subject", "specific scene details", "natural imperfections"],
|
|
3118
3140
|
"character": ["consistent character identity", "turnaround views", "expression close-ups"],
|
|
@@ -3451,7 +3473,7 @@ def render_prompt(spec: dict) -> str:
|
|
|
3451
3473
|
brief_block,
|
|
3452
3474
|
f"Style / quality envelope for diagram: {diagram_style}. This only controls presentation polish; the user brief wins.",
|
|
3453
3475
|
f"Quality controls: crisp typography, clear node grouping, balanced white space, precise alignment, high contrast, restrained palette ({diagram_palette}).",
|
|
3454
|
-
f"Recommended structure: {layout}. Use boxes, groups, arrows, and a feedback loop only where they match the brief.",
|
|
3476
|
+
f"Recommended structure: {layout}. Use boxes, groups, arrows, and a feedback loop only where they match the brief; do not add a legend or extra explanatory labels unless the brief asks for them.",
|
|
3455
3477
|
"Do not over-template the diagram. Preserve the user's named modules, reading order, and product-review context.",
|
|
3456
3478
|
f"Must include: {must_include}.",
|
|
3457
3479
|
text_block,
|
|
@@ -4870,6 +4892,8 @@ def visual_case_compile(case: dict) -> dict:
|
|
|
4870
4892
|
if tool in {"convert", "rewrite"}:
|
|
4871
4893
|
compiled = compile_visual_case(case, target=case.get("target") or "codex-image")
|
|
4872
4894
|
return with_compiled_items({**compiled, "tool": tool}, [{"id": case.get("id") or tool, **compiled}])
|
|
4895
|
+
if tool == "edit":
|
|
4896
|
+
return compile_visual_edit_case(case)
|
|
4873
4897
|
if tool == "variants":
|
|
4874
4898
|
return compile_visual_variants_case(case)
|
|
4875
4899
|
if tool == "series":
|
|
@@ -5128,6 +5152,51 @@ def compile_visual_variants_case(case: dict) -> dict:
|
|
|
5128
5152
|
return with_compiled_items({"tool": "variants", "count": len(items)}, items)
|
|
5129
5153
|
|
|
5130
5154
|
|
|
5155
|
+
def compile_visual_edit_case(case: dict) -> dict:
|
|
5156
|
+
goal = str(case.get("goal") or visual_case_request(case))
|
|
5157
|
+
if not goal:
|
|
5158
|
+
raise ValueError("edit case 缺少 goal/request 字段")
|
|
5159
|
+
references = [parse_reference(value) for value in case_list(case.get("reference") or case.get("references"))]
|
|
5160
|
+
preserve = case_list(case.get("preserve")) or ["main subject identity, silhouette, material cues, and composition anchors"]
|
|
5161
|
+
changes = case_list(case.get("change")) or [goal]
|
|
5162
|
+
required_text = case_list(case.get("text") or case.get("required_text"))
|
|
5163
|
+
aspect = str(case.get("aspect") or "3:4")
|
|
5164
|
+
asset_type = str(case.get("asset_type") or "product")
|
|
5165
|
+
quality = str(case.get("quality") or "high")
|
|
5166
|
+
reference_block = "; ".join(f"{item['role']}={item['ref']}" for item in references) or "provided reference image(s)"
|
|
5167
|
+
prompt = "\n".join(
|
|
5168
|
+
[
|
|
5169
|
+
f"Edit the provided reference image(s) into a {aspect} {asset_type} result for: {goal}.",
|
|
5170
|
+
f"References: {reference_block}.",
|
|
5171
|
+
"Preserve exactly: " + "; ".join(preserve) + ".",
|
|
5172
|
+
"Change only: " + "; ".join(changes) + ".",
|
|
5173
|
+
f"Visual target: {case.get('style') or 'production-quality realistic edit, consistent lighting, no visible seams'}; quality={quality}.",
|
|
5174
|
+
exact_text_block(required_text),
|
|
5175
|
+
"Avoid: identity drift; unwanted background changes; mismatched perspective; fake logos; garbled text; low-resolution artifacts.",
|
|
5176
|
+
]
|
|
5177
|
+
)
|
|
5178
|
+
lint = lint_prompt(prompt, asset_type, quality, required_text)
|
|
5179
|
+
spec = {
|
|
5180
|
+
"asset_type": asset_type,
|
|
5181
|
+
"aspect": aspect,
|
|
5182
|
+
"template_id": "edit",
|
|
5183
|
+
"required_text": required_text,
|
|
5184
|
+
"strict_text": False,
|
|
5185
|
+
"quality": quality,
|
|
5186
|
+
}
|
|
5187
|
+
compiled = {
|
|
5188
|
+
"spec": spec,
|
|
5189
|
+
"prompt": prompt,
|
|
5190
|
+
"prompt_digest": prompt_digest(prompt),
|
|
5191
|
+
"lint": lint,
|
|
5192
|
+
"intent_check": [],
|
|
5193
|
+
"handoff": None,
|
|
5194
|
+
"text_overlay_spec": None,
|
|
5195
|
+
"acceptance_criteria": [],
|
|
5196
|
+
}
|
|
5197
|
+
return with_compiled_items({"tool": "edit", **compiled}, [{"id": "edit-01", **compiled}])
|
|
5198
|
+
|
|
5199
|
+
|
|
5131
5200
|
def compile_visual_series_case(case: dict) -> dict:
|
|
5132
5201
|
raw_items = case.get("briefs") or case.get("items") or case.get("series") or []
|
|
5133
5202
|
if isinstance(raw_items, str):
|
|
@@ -5370,6 +5439,27 @@ def visual_case_expectation_findings(case: dict, compiled: dict) -> list[dict]:
|
|
|
5370
5439
|
"message": f"期望 template_id={expected_template},实际={spec.get('template_id')}",
|
|
5371
5440
|
}
|
|
5372
5441
|
)
|
|
5442
|
+
expected_templates = case_list(case.get("expect_template_ids"))
|
|
5443
|
+
if expected_templates:
|
|
5444
|
+
actual = [str(item.get("template_id") or "") for item in specs]
|
|
5445
|
+
if actual != expected_templates:
|
|
5446
|
+
findings.append(
|
|
5447
|
+
{
|
|
5448
|
+
"severity": "error",
|
|
5449
|
+
"rule": "case.template_ids_mismatch",
|
|
5450
|
+
"message": f"期望 template_ids={expected_templates},实际={actual}",
|
|
5451
|
+
}
|
|
5452
|
+
)
|
|
5453
|
+
|
|
5454
|
+
if case.get("expect_safety_rewrite"):
|
|
5455
|
+
if not any(item.get("safety_rewrite") for item in specs):
|
|
5456
|
+
findings.append(
|
|
5457
|
+
{
|
|
5458
|
+
"severity": "error",
|
|
5459
|
+
"rule": "case.safety_rewrite_missing",
|
|
5460
|
+
"message": "期望触发安全改写,但 spec.safety_rewrite 为空",
|
|
5461
|
+
}
|
|
5462
|
+
)
|
|
5373
5463
|
|
|
5374
5464
|
labels_by_item = [set(item.get("required_text") or []) for item in specs]
|
|
5375
5465
|
labels = set().union(*labels_by_item) if labels_by_item else set()
|