@yuhan1124/draw-prompt 0.4.7 → 0.4.8

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 CHANGED
@@ -69,7 +69,8 @@ npx --yes --registry=https://registry.npmjs.org/ @yuhan1124/draw-prompt@latest i
69
69
 
70
70
  `install-skill` 默认复制 npm 包内的运行文件,避免软链到 npx 缓存导致后续路径失效。它只复制
71
71
  `SKILL.md`、CLI、references 和必要元数据;开发仓库里的 `tests/`、`tmp/`、`golden-cases.jsonl`、
72
- `visual-cases.jsonl` 不会进入 npm 包或安装目录。
72
+ `visual-cases.jsonl` 不会进入 npm 包或安装目录。安装后可直接跑 `doctor`,它会检查
73
+ 包文件、版本一致性、核心单图转化和一条真实长输入 `compose` 链路。
73
74
 
74
75
  开发者本地调试也可以软链 repo:
75
76
 
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.7
11
+ version: 0.4.8
12
12
  openclaw:
13
13
  anyBins: ["uv", "python3"]
14
14
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuhan1124/draw-prompt",
3
- "version": "0.4.7",
3
+ "version": "0.4.8",
4
4
  "description": "Convert natural-language image requests into high-quality gpt-image-2 prompts and Codex handoff blocks.",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -136,7 +136,7 @@ def ensure_home() -> None:
136
136
 
137
137
 
138
138
  SCHEMA_VERSION = 1
139
- COMPILER_VERSION = "0.4.7"
139
+ COMPILER_VERSION = "0.4.8"
140
140
 
141
141
 
142
142
  PACKAGED_SKILL_FILES = [
@@ -211,6 +211,29 @@ DOCTOR_CASES = [
211
211
  },
212
212
  ]
213
213
 
214
+ DOCTOR_COMPOSE_CASES = [
215
+ {
216
+ "id": "long-input-product-plan",
217
+ "request": (
218
+ "请把下面这段产品方案整理成 4 张图用于路演:我们要做一个 AI 生图 Prompt 转化 skill。"
219
+ "目标用户是经常把长文档、产品需求、运营数据、架构说明变成图片的研发和运营同学。"
220
+ "当前痛点是直接把长输入丢给模型容易跑偏,常见问题包括风格不稳定、中文标签遗漏、架构图被画成 UI、产品图漏卖点、活动战报颜色跑偏。"
221
+ "方案包括:场景识别、风格预设、意图保持检查、质量门、真实 Image 2 验证、样本反馈学习。"
222
+ "上线标准是安装后 doctor 通过、常见场景开箱高质量、NPM 公网安装可用。"
223
+ ),
224
+ "max_images": 4,
225
+ "style_preset": "corporate",
226
+ "expect_count": 4,
227
+ "expect_no_asset_type": "illustration",
228
+ "expect_required_text_by_index": {
229
+ 0: ["目标用户"],
230
+ 1: ["当前痛点"],
231
+ 2: ["方案", "场景识别", "风格预设", "质量门"],
232
+ 3: ["上线标准", "NPM 公网安装可用"],
233
+ },
234
+ }
235
+ ]
236
+
214
237
 
215
238
  def package_root() -> Path:
216
239
  return Path(__file__).resolve().parent.parent
@@ -4887,6 +4910,30 @@ def split_document_sections(text: str, max_images: int) -> list[str]:
4887
4910
  if buf:
4888
4911
  chunks.append(" ".join(buf).strip())
4889
4912
 
4913
+ if len(chunks) <= 1:
4914
+ semantic_patterns = [
4915
+ r"(目标用户(?:是|:|:)[^。;;\n]+)",
4916
+ r"(用户场景(?:是|:|:)[^。;;\n]+)",
4917
+ r"(当前痛点(?:是|:|:)[^。;;\n]+)",
4918
+ r"(主要问题(?:是|:|:)[^。;;\n]+)",
4919
+ r"(方案(?:包括|是|:|:)[^。;;\n]+)",
4920
+ r"(核心能力(?:包括|是|:|:)[^。;;\n]+)",
4921
+ r"(上线标准(?:是|:|:)[^。;;\n]+)",
4922
+ r"(验收标准(?:是|:|:)[^。;;\n]+)",
4923
+ ]
4924
+ semantic_matches: list[tuple[int, str]] = []
4925
+ seen_spans: set[tuple[int, int]] = set()
4926
+ for pattern in semantic_patterns:
4927
+ for match in re.finditer(pattern, text):
4928
+ span = match.span(1)
4929
+ if span in seen_spans:
4930
+ continue
4931
+ seen_spans.add(span)
4932
+ semantic_matches.append((span[0], match.group(1).strip()))
4933
+ semantic_chunks = [chunk for _, chunk in sorted(semantic_matches, key=lambda item: item[0])]
4934
+ if len(semantic_chunks) >= 2:
4935
+ chunks = semantic_chunks
4936
+
4890
4937
  if len(chunks) <= 1:
4891
4938
  sentences = [s.strip() for s in re.split(r"(?<=[。!?!?;;])\s*", text) if s.strip()]
4892
4939
  chunks = []
@@ -4908,6 +4955,8 @@ def split_document_sections(text: str, max_images: int) -> list[str]:
4908
4955
 
4909
4956
  def choose_compose_asset(chunk: str, index: int) -> str:
4910
4957
  lower = chunk.lower()
4958
+ if re.search(r"^(?:目标用户|用户场景|当前痛点|主要问题|上线标准|验收标准)", chunk):
4959
+ return "infographic"
4911
4960
  if any(k in lower for k in ["架构", "系统", "模块", "链路", "rag", "llm", "retriever", "pipeline"]):
4912
4961
  return "diagram"
4913
4962
  if any(k in lower for k in ["数据", "指标", "报表", "趋势", "占比", "转化率", "漏斗", "图表"]):
@@ -4922,7 +4971,9 @@ def choose_compose_asset(chunk: str, index: int) -> str:
4922
4971
  return "character"
4923
4972
  if index == 0 and any(k in lower for k in ["标题", "主题", "发布", "活动", "封面", "总结"]):
4924
4973
  return "poster"
4925
- return "illustration"
4974
+ if any(k in lower for k in ["插画", "场景插图", "故事", "氛围图", "场景图", "scene illustration"]):
4975
+ return "illustration"
4976
+ return "infographic"
4926
4977
 
4927
4978
 
4928
4979
  def infer_compose_style(text: str) -> str:
@@ -4946,6 +4997,13 @@ def extract_visual_labels(chunk: str, asset_type: str, limit: int = 5) -> list[s
4946
4997
 
4947
4998
  for match in re.findall(r'"([^"\n]{1,28})"|“([^”\n]{1,28})”|「([^」\n]{1,28})」', chunk):
4948
4999
  add(next((m for m in match if m), ""))
5000
+ for match in re.finditer(r"(目标用户|用户场景|当前痛点|主要问题|方案|核心能力|上线标准|验收标准)\s*(?:是|包括|包含|:|:)\s*([^。;;\n]{2,140})", chunk):
5001
+ add(match.group(1))
5002
+ value = match.group(2)
5003
+ for part in re.split(r"[、,,/|]", value):
5004
+ part = re.sub(r"^(?:和|与|及|以及|and)\s*", "", part.strip(), flags=re.IGNORECASE)
5005
+ if 1 < len(part) <= 18:
5006
+ add(part)
4949
5007
  for match in re.findall(r"\b[A-Z][A-Za-z0-9_-]{1,20}\b", chunk):
4950
5008
  add(match)
4951
5009
  for match in re.findall(r"(?:标题|主题|模块|步骤|节点|页面)[::\s]*([^,。;;\n]{2,24})", chunk):
@@ -4964,7 +5022,7 @@ def compose_purpose(asset_type: str, index: int) -> str:
4964
5022
  "ui": "界面概念图",
4965
5023
  "product": "产品视觉图",
4966
5024
  "character": "角色设定图",
4967
- "illustration": "场景说明图",
5025
+ "illustration": "场景插画",
4968
5026
  }
4969
5027
  return f"{index}. {purpose_map.get(asset_type, '配图')}"
4970
5028
 
@@ -5796,6 +5854,68 @@ def doctor_check_case(case: dict) -> dict:
5796
5854
  }
5797
5855
 
5798
5856
 
5857
+ def doctor_check_compose_case(case: dict) -> dict:
5858
+ findings: list[dict] = []
5859
+ text = str(case.get("request") or "")
5860
+ max_images = int(case.get("max_images") or 4)
5861
+ chunks = split_document_sections(text, max_images)
5862
+ shared_style = infer_compose_style(text)
5863
+ visual_plan: list[dict] = []
5864
+ for idx, chunk in enumerate(chunks, start=1):
5865
+ asset_type = choose_compose_asset(chunk, idx - 1)
5866
+ labels = extract_visual_labels(chunk, asset_type)
5867
+ purpose = compose_purpose(asset_type, idx)
5868
+ compiled = compile_visual_case(
5869
+ {
5870
+ "id": f"compose-{idx:02d}",
5871
+ "request": f"{purpose}。根据这段内容生成对应画面:{chunk}",
5872
+ "asset_type": asset_type,
5873
+ "style": shared_style,
5874
+ "style_preset": case.get("style_preset"),
5875
+ "text": labels if asset_type in {"diagram", "infographic", "ui"} else [],
5876
+ "target": "raw",
5877
+ "tags": "compose,long-input",
5878
+ },
5879
+ target="raw",
5880
+ include_handoff=False,
5881
+ )
5882
+ visual_plan.append({"chunk": chunk, "compiled": compiled})
5883
+ for item in compiled["lint"]:
5884
+ findings.append({"severity": item["severity"], "rule": f"compose.{idx}.lint.{item['rule']}", "message": item["message"]})
5885
+ for item in compiled["intent_check"]:
5886
+ findings.append({"severity": item["severity"], "rule": f"compose.{idx}.intent.{item['rule']}", "message": item["message"]})
5887
+
5888
+ if case.get("expect_count") and len(visual_plan) != int(case["expect_count"]):
5889
+ doctor_add(findings, "error", "compose.count", f"期望 {case['expect_count']} 张图,实际 {len(visual_plan)}")
5890
+ forbidden_asset = case.get("expect_no_asset_type")
5891
+ if forbidden_asset:
5892
+ for idx, item in enumerate(visual_plan, start=1):
5893
+ asset_type = item["compiled"]["spec"].get("asset_type")
5894
+ if asset_type == forbidden_asset:
5895
+ doctor_add(findings, "error", "compose.asset_type", f"第 {idx} 张图不应路由为 {forbidden_asset}")
5896
+ for raw_index, expected_texts in (case.get("expect_required_text_by_index") or {}).items():
5897
+ index = int(raw_index)
5898
+ if index >= len(visual_plan):
5899
+ doctor_add(findings, "error", "compose.index_missing", f"缺少第 {index + 1} 张图")
5900
+ continue
5901
+ labels = set(visual_plan[index]["compiled"]["spec"].get("required_text") or [])
5902
+ prompt = visual_plan[index]["compiled"]["prompt"]
5903
+ for text_item in expected_texts:
5904
+ if text_item not in labels:
5905
+ doctor_add(findings, "error", "compose.required_text_missing", f"第 {index + 1} 张图缺少必显文字:{text_item}")
5906
+ elif f'"{text_item}"' not in prompt:
5907
+ doctor_add(findings, "error", "compose.required_text_not_quoted", f"第 {index + 1} 张图 Prompt 未逐字引用:{text_item}")
5908
+ return {
5909
+ "name": f"compose-{case['id']}",
5910
+ "pass": not has_lint_error(findings),
5911
+ "count": len(visual_plan),
5912
+ "asset_types": [item["compiled"]["spec"].get("asset_type") for item in visual_plan],
5913
+ "prompt_digests": [item["compiled"]["prompt_digest"] for item in visual_plan],
5914
+ "required_text": [item["compiled"]["spec"].get("required_text") or [] for item in visual_plan],
5915
+ "findings": findings,
5916
+ }
5917
+
5918
+
5799
5919
  def cmd_doctor(args: argparse.Namespace) -> int:
5800
5920
  root = package_root()
5801
5921
  checks = [doctor_check_package(root)]
@@ -5804,6 +5924,11 @@ def cmd_doctor(args: argparse.Namespace) -> int:
5804
5924
  checks.append(doctor_check_case(case))
5805
5925
  except Exception as exc:
5806
5926
  checks.append({"name": case.get("id", "case"), "pass": False, "findings": [{"severity": "error", "rule": "case.exception", "message": str(exc)}]})
5927
+ for case in DOCTOR_COMPOSE_CASES:
5928
+ try:
5929
+ checks.append(doctor_check_compose_case(case))
5930
+ except Exception as exc:
5931
+ checks.append({"name": f"compose-{case.get('id', 'case')}", "pass": False, "findings": [{"severity": "error", "rule": "compose.exception", "message": str(exc)}]})
5807
5932
  passed = all(item["pass"] for item in checks)
5808
5933
  summary = {"pass": passed, "version": COMPILER_VERSION, "checks": len(checks), "failed": len([item for item in checks if not item["pass"]])}
5809
5934
  if args.json: