@optima-chat/optima-agent 0.9.8 → 0.9.9

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.
Files changed (171) hide show
  1. package/.claude/settings.local.json +166 -0
  2. package/.claude/skills/.kb-skills-managed.json +9 -9
  3. package/.claude/skills/ads/SKILL.md +244 -244
  4. package/.claude/skills/ads/template/campaign/CREATIVES.md +18 -18
  5. package/.claude/skills/ads/template/campaign/NOTES.md +10 -10
  6. package/.claude/skills/ads/template/campaign/STRATEGY.md +29 -29
  7. package/.claude/skills/ads/template/user/ADS.md +29 -29
  8. package/.claude/skills/ads/template/user/LEARNINGS.md +15 -15
  9. package/.claude/skills/ads/template/user/PROGRESS.md +20 -20
  10. package/.claude/skills/ads/template/user/README.md +25 -25
  11. package/.claude/skills/ads/template/user/assets/.gitignore +2 -2
  12. package/.claude/skills/bi/SKILL.md +131 -131
  13. package/.claude/skills/browser/SKILL.md +201 -201
  14. package/.claude/skills/channels/SKILL.md +188 -188
  15. package/.claude/skills/collection/SKILL.md +88 -88
  16. package/.claude/skills/douyin/SKILL.md +408 -408
  17. package/.claude/skills/ffmpeg/SKILL.md +164 -164
  18. package/.claude/skills/gen/SKILL.md +279 -279
  19. package/.claude/skills/growth/SKILL.md +90 -90
  20. package/.claude/skills/growth/template/ACCOUNTS.md +14 -14
  21. package/.claude/skills/growth/template/CALENDAR.md +7 -7
  22. package/.claude/skills/growth/template/COMMENTS.md +7 -7
  23. package/.claude/skills/growth/template/GROWTH.md +37 -37
  24. package/.claude/skills/growth/template/PROGRESS.md +4 -4
  25. package/.claude/skills/growth/template/README.md +20 -20
  26. package/.claude/skills/growth/template/TOPICS.md +7 -7
  27. package/.claude/skills/homepage/SKILL.md +177 -177
  28. package/.claude/skills/i18n/SKILL.md +517 -517
  29. package/.claude/skills/ingesting-sources/SKILL.md +94 -94
  30. package/.claude/skills/initializing-kb/SKILL.md +117 -117
  31. package/.claude/skills/instagram/SKILL.md +321 -321
  32. package/.claude/skills/inventory/SKILL.md +328 -328
  33. package/.claude/skills/kol-outreach/SKILL.md +232 -232
  34. package/.claude/skills/kol-outreach/template/campaign/CONFIG.md +60 -60
  35. package/.claude/skills/kol-outreach/template/campaign/KOLS.md +6 -6
  36. package/.claude/skills/kol-outreach/template/campaign/PROGRESS.md +3 -3
  37. package/.claude/skills/kol-outreach/template/campaign/TEMPLATES.md +88 -88
  38. package/.claude/skills/kol-outreach/template/merchant/BRAND.md +36 -36
  39. package/.claude/skills/kol-outreach/template/merchant/CAMPAIGNS.md +6 -6
  40. package/.claude/skills/kol-outreach/template/merchant/MERCHANT_LIMITS.md +16 -16
  41. package/.claude/skills/kol-outreach/template/merchant/PROGRESS.md +4 -4
  42. package/.claude/skills/kol-outreach/template/merchant/README.md +20 -20
  43. package/.claude/skills/linting-the-wiki/SKILL.md +68 -68
  44. package/.claude/skills/logistics/SKILL.md +180 -180
  45. package/.claude/skills/markdown-pdf/SKILL.md +72 -72
  46. package/.claude/skills/merchant/SKILL.md +110 -110
  47. package/.claude/skills/multigrid-poster/SKILL.md +192 -192
  48. package/.claude/skills/multigrid-poster/layouts/2x2.json +34 -34
  49. package/.claude/skills/multigrid-poster/layouts/3x3.json +43 -43
  50. package/.claude/skills/multigrid-poster/scripts/compose.py +116 -116
  51. package/.claude/skills/order/SKILL.md +452 -452
  52. package/.claude/skills/product/SKILL.md +379 -379
  53. package/.claude/skills/product-page/SKILL.md +106 -106
  54. package/.claude/skills/querying-the-wiki/SKILL.md +59 -59
  55. package/.claude/skills/reddit/SKILL.md +277 -277
  56. package/.claude/skills/review/SKILL.md +321 -321
  57. package/.claude/skills/scout/SKILL.md +575 -462
  58. package/.claude/skills/sentinel/SKILL.md +281 -281
  59. package/.claude/skills/shein/SKILL.md +246 -246
  60. package/.claude/skills/shipping/SKILL.md +200 -200
  61. package/.claude/skills/shop-content/SKILL.md +101 -101
  62. package/.claude/skills/shopify/SKILL.md +282 -282
  63. package/.claude/skills/skillify/SKILL.md +114 -114
  64. package/.claude/skills/taobao/SKILL.md +238 -238
  65. package/.claude/skills/tiktok/SKILL.md +381 -381
  66. package/.claude/skills/twitter/SKILL.md +302 -302
  67. package/.claude/skills/updating-related-pages/SKILL.md +65 -65
  68. package/.claude/skills/video-edit/SKILL.md +143 -143
  69. package/.claude/skills/video-gen/SKILL.md +548 -548
  70. package/.claude/skills/video-gen/templates/INDEX.md +78 -78
  71. package/.claude/skills/video-gen/templates/before-after-beauty.md +183 -183
  72. package/.claude/skills/video-gen/templates/drama-fmcg.md +183 -183
  73. package/.claude/skills/video-gen/templates/kol-reaction-food.md +193 -193
  74. package/.claude/skills/video-gen/templates/multi-point-apparel.md +185 -185
  75. package/.claude/skills/video-gen/templates/pain-solution-home.md +184 -184
  76. package/.claude/skills/video-gen/templates/pdp-360-showcase.md +189 -189
  77. package/.claude/skills/video-gen/templates/pdp-feature-highlight.md +182 -182
  78. package/.claude/skills/video-gen/templates/scene-digital.md +183 -183
  79. package/.claude/skills/wechat/SKILL.md +174 -174
  80. package/.claude/skills/xhs/SKILL.md +170 -170
  81. package/README.md +276 -276
  82. package/dist/bin/bi-cli.js +0 -0
  83. package/dist/bin/browser-cli.js +0 -0
  84. package/dist/bin/comfy.d.ts +3 -0
  85. package/dist/bin/comfy.d.ts.map +1 -0
  86. package/dist/bin/comfy.js +3 -0
  87. package/dist/bin/comfy.js.map +1 -0
  88. package/dist/bin/commerce.js +0 -0
  89. package/dist/bin/gen.js +0 -0
  90. package/dist/bin/google-ads.js +0 -0
  91. package/dist/bin/growth.d.ts +3 -0
  92. package/dist/bin/growth.d.ts.map +1 -0
  93. package/dist/bin/growth.js +3 -0
  94. package/dist/bin/growth.js.map +1 -0
  95. package/dist/bin/logistics.js +0 -0
  96. package/dist/bin/optima.js +26 -26
  97. package/dist/bin/scout.js +0 -0
  98. package/dist/bin/sentinel.js +0 -0
  99. package/dist/bin/serve.js +23 -23
  100. package/dist/bin/shopify.js +0 -0
  101. package/dist/src/agent.js +4 -4
  102. package/dist/src/hooks-loader.d.ts +6 -0
  103. package/dist/src/hooks-loader.d.ts.map +1 -0
  104. package/dist/src/hooks-loader.js +215 -0
  105. package/dist/src/hooks-loader.js.map +1 -0
  106. package/dist/src/system-prompt.d.ts.map +1 -1
  107. package/dist/src/system-prompt.js +173 -169
  108. package/dist/src/system-prompt.js.map +1 -1
  109. package/dist/src/tools/memory.js +10 -10
  110. package/dist/src/ui/App.d.ts +6 -0
  111. package/dist/src/ui/App.d.ts.map +1 -0
  112. package/dist/src/ui/App.js +164 -0
  113. package/dist/src/ui/App.js.map +1 -0
  114. package/dist/src/ui/components/Composer.d.ts +10 -0
  115. package/dist/src/ui/components/Composer.d.ts.map +1 -0
  116. package/dist/src/ui/components/Composer.js +13 -0
  117. package/dist/src/ui/components/Composer.js.map +1 -0
  118. package/dist/src/ui/components/Header.d.ts +7 -0
  119. package/dist/src/ui/components/Header.d.ts.map +1 -0
  120. package/dist/src/ui/components/Header.js +7 -0
  121. package/dist/src/ui/components/Header.js.map +1 -0
  122. package/dist/src/ui/components/Message.d.ts +12 -0
  123. package/dist/src/ui/components/Message.d.ts.map +1 -0
  124. package/dist/src/ui/components/Message.js +21 -0
  125. package/dist/src/ui/components/Message.js.map +1 -0
  126. package/dist/src/ui/components/MessageList.d.ts +9 -0
  127. package/dist/src/ui/components/MessageList.d.ts.map +1 -0
  128. package/dist/src/ui/components/MessageList.js +18 -0
  129. package/dist/src/ui/components/MessageList.js.map +1 -0
  130. package/dist/src/ui/components/Spinner.d.ts +6 -0
  131. package/dist/src/ui/components/Spinner.d.ts.map +1 -0
  132. package/dist/src/ui/components/Spinner.js +7 -0
  133. package/dist/src/ui/components/Spinner.js.map +1 -0
  134. package/dist/src/ui/components/StatusBar.d.ts +11 -0
  135. package/dist/src/ui/components/StatusBar.d.ts.map +1 -0
  136. package/dist/src/ui/components/StatusBar.js +7 -0
  137. package/dist/src/ui/components/StatusBar.js.map +1 -0
  138. package/dist/src/ui/components/index.d.ts +7 -0
  139. package/dist/src/ui/components/index.d.ts.map +1 -0
  140. package/dist/src/ui/components/index.js +7 -0
  141. package/dist/src/ui/components/index.js.map +1 -0
  142. package/dist/src/ui/headless.js +7 -7
  143. package/dist/src/validation/error-formatter.d.ts +21 -0
  144. package/dist/src/validation/error-formatter.d.ts.map +1 -0
  145. package/dist/src/validation/error-formatter.js +98 -0
  146. package/dist/src/validation/error-formatter.js.map +1 -0
  147. package/dist/src/validation/index.d.ts +10 -0
  148. package/dist/src/validation/index.d.ts.map +1 -0
  149. package/dist/src/validation/index.js +10 -0
  150. package/dist/src/validation/index.js.map +1 -0
  151. package/dist/src/validation/json-validator.d.ts +25 -0
  152. package/dist/src/validation/json-validator.d.ts.map +1 -0
  153. package/dist/src/validation/json-validator.js +173 -0
  154. package/dist/src/validation/json-validator.js.map +1 -0
  155. package/dist/src/validation/schema.d.ts +353 -0
  156. package/dist/src/validation/schema.d.ts.map +1 -0
  157. package/dist/src/validation/schema.js +57 -0
  158. package/dist/src/validation/schema.js.map +1 -0
  159. package/dist/src/validation/suggestions.d.ts +25 -0
  160. package/dist/src/validation/suggestions.d.ts.map +1 -0
  161. package/dist/src/validation/suggestions.js +144 -0
  162. package/dist/src/validation/suggestions.js.map +1 -0
  163. package/dist/src/validation/types.d.ts +40 -0
  164. package/dist/src/validation/types.d.ts.map +1 -0
  165. package/dist/src/validation/types.js +5 -0
  166. package/dist/src/validation/types.js.map +1 -0
  167. package/dist/src/validation/yaml-validator.d.ts +25 -0
  168. package/dist/src/validation/yaml-validator.d.ts.map +1 -0
  169. package/dist/src/validation/yaml-validator.js +177 -0
  170. package/dist/src/validation/yaml-validator.js.map +1 -0
  171. package/package.json +79 -79
@@ -1,34 +1,34 @@
1
- {
2
- "_comment": "通用 2×2 网格布局 — 适合 4 张 cell 的所有 intent (创业故事/对比测评/教程/场景)",
3
- "canvas_size": [1242, 1660],
4
- "cells": {
5
- "positions": [[0, 0], [621, 0], [0, 830], [621, 830]],
6
- "sizes": [[621, 830], [621, 830], [621, 830], [621, 830]]
7
- },
8
- "text_zones": {
9
- "title": {
10
- "_comment": "中央偏上 2 行标题 - 8-12 字 / 行最佳",
11
- "font": "shared/fonts/MaShanZheng-Regular.ttf",
12
- "size": 110,
13
- "color": "#FFB940",
14
- "stroke_w": 8,
15
- "stroke_color": "#D63D3D",
16
- "lines": [
17
- {"position": [621, 480], "anchor": "mm"},
18
- {"position": [621, 620], "anchor": "mm"}
19
- ]
20
- },
21
- "caption": {
22
- "_comment": "底部 2 行 caption - 15-20 字 / 行最佳",
23
- "font": "shared/fonts/MaShanZheng-Regular.ttf",
24
- "size": 78,
25
- "color": "#FFB940",
26
- "stroke_w": 6,
27
- "stroke_color": "#D63D3D",
28
- "lines": [
29
- {"position": [621, 1340], "anchor": "mm"},
30
- {"position": [621, 1450], "anchor": "mm"}
31
- ]
32
- }
33
- }
34
- }
1
+ {
2
+ "_comment": "通用 2×2 网格布局 — 适合 4 张 cell 的所有 intent (创业故事/对比测评/教程/场景)",
3
+ "canvas_size": [1242, 1660],
4
+ "cells": {
5
+ "positions": [[0, 0], [621, 0], [0, 830], [621, 830]],
6
+ "sizes": [[621, 830], [621, 830], [621, 830], [621, 830]]
7
+ },
8
+ "text_zones": {
9
+ "title": {
10
+ "_comment": "中央偏上 2 行标题 - 8-12 字 / 行最佳",
11
+ "font": "shared/fonts/MaShanZheng-Regular.ttf",
12
+ "size": 110,
13
+ "color": "#FFB940",
14
+ "stroke_w": 8,
15
+ "stroke_color": "#D63D3D",
16
+ "lines": [
17
+ {"position": [621, 480], "anchor": "mm"},
18
+ {"position": [621, 620], "anchor": "mm"}
19
+ ]
20
+ },
21
+ "caption": {
22
+ "_comment": "底部 2 行 caption - 15-20 字 / 行最佳",
23
+ "font": "shared/fonts/MaShanZheng-Regular.ttf",
24
+ "size": 78,
25
+ "color": "#FFB940",
26
+ "stroke_w": 6,
27
+ "stroke_color": "#D63D3D",
28
+ "lines": [
29
+ {"position": [621, 1340], "anchor": "mm"},
30
+ {"position": [621, 1450], "anchor": "mm"}
31
+ ]
32
+ }
33
+ }
34
+ }
@@ -1,43 +1,43 @@
1
- {
2
- "_comment": "通用 3×3 网格布局 - 适合 9 张 cell (商品清单 / 多角度展示)",
3
- "canvas_size": [1242, 1660],
4
- "cells": {
5
- "_comment": "9 格 414×420。顶部 200px 标题区,cells y=200..1460,底部 200px caption 区。3×420 + 200×2 = 1660 = canvas_h ✓",
6
- "positions": [
7
- [0, 200], [414, 200], [828, 200],
8
- [0, 620], [414, 620], [828, 620],
9
- [0, 1040], [414, 1040], [828, 1040]
10
- ],
11
- "sizes": [
12
- [414, 420], [414, 420], [414, 420],
13
- [414, 420], [414, 420], [414, 420],
14
- [414, 420], [414, 420], [414, 420]
15
- ]
16
- },
17
- "text_zones": {
18
- "title": {
19
- "_comment": "顶部白边 2 行标题(y < 200 区间)",
20
- "font": "shared/fonts/MaShanZheng-Regular.ttf",
21
- "size": 78,
22
- "color": "#FFB940",
23
- "stroke_w": 6,
24
- "stroke_color": "#D63D3D",
25
- "lines": [
26
- {"position": [621, 60], "anchor": "mm"},
27
- {"position": [621, 150], "anchor": "mm"}
28
- ]
29
- },
30
- "caption": {
31
- "_comment": "底部白边 2 行 caption(cells 结束于 y=1460,留 200px)",
32
- "font": "shared/fonts/MaShanZheng-Regular.ttf",
33
- "size": 60,
34
- "color": "#FFB940",
35
- "stroke_w": 5,
36
- "stroke_color": "#D63D3D",
37
- "lines": [
38
- {"position": [621, 1530], "anchor": "mm"},
39
- {"position": [621, 1610], "anchor": "mm"}
40
- ]
41
- }
42
- }
43
- }
1
+ {
2
+ "_comment": "通用 3×3 网格布局 - 适合 9 张 cell (商品清单 / 多角度展示)",
3
+ "canvas_size": [1242, 1660],
4
+ "cells": {
5
+ "_comment": "9 格 414×420。顶部 200px 标题区,cells y=200..1460,底部 200px caption 区。3×420 + 200×2 = 1660 = canvas_h ✓",
6
+ "positions": [
7
+ [0, 200], [414, 200], [828, 200],
8
+ [0, 620], [414, 620], [828, 620],
9
+ [0, 1040], [414, 1040], [828, 1040]
10
+ ],
11
+ "sizes": [
12
+ [414, 420], [414, 420], [414, 420],
13
+ [414, 420], [414, 420], [414, 420],
14
+ [414, 420], [414, 420], [414, 420]
15
+ ]
16
+ },
17
+ "text_zones": {
18
+ "title": {
19
+ "_comment": "顶部白边 2 行标题(y < 200 区间)",
20
+ "font": "shared/fonts/MaShanZheng-Regular.ttf",
21
+ "size": 78,
22
+ "color": "#FFB940",
23
+ "stroke_w": 6,
24
+ "stroke_color": "#D63D3D",
25
+ "lines": [
26
+ {"position": [621, 60], "anchor": "mm"},
27
+ {"position": [621, 150], "anchor": "mm"}
28
+ ]
29
+ },
30
+ "caption": {
31
+ "_comment": "底部白边 2 行 caption(cells 结束于 y=1460,留 200px)",
32
+ "font": "shared/fonts/MaShanZheng-Regular.ttf",
33
+ "size": 60,
34
+ "color": "#FFB940",
35
+ "stroke_w": 5,
36
+ "stroke_color": "#D63D3D",
37
+ "lines": [
38
+ {"position": [621, 1530], "anchor": "mm"},
39
+ {"position": [621, 1610], "anchor": "mm"}
40
+ ]
41
+ }
42
+ }
43
+ }
@@ -1,116 +1,116 @@
1
- #!/usr/bin/env python3
2
- """multigrid-poster compose — Pillow 版渲染器
3
-
4
- 输入:layout.json + N 张 cell 图片 + 标题文字 + caption 文字
5
- 输出:1242×1660 PNG(小红书封面标准)
6
-
7
- 用法:
8
- python compose.py \
9
- --layout <SKILL_DIR>/layouts/2x2.json \
10
- --cells cell_0.png cell_1.png cell_2.png cell_3.png \
11
- --title-line "26岁一个人创业" \
12
- --title-line "跨境电商月入10w+" \
13
- --caption-line "只需要一部手机就可以完成!" \
14
- --caption-line "跨境人的必备app推荐" \
15
- --output cover.png
16
-
17
- 依赖:Pillow (容器自带,无需额外安装)
18
- 字体:从 SKILL_DIR/shared/fonts/ 加载(layout.json 里指定)
19
- """
20
- import argparse
21
- import json
22
- import sys
23
- from pathlib import Path
24
- from PIL import Image, ImageDraw, ImageFont
25
-
26
-
27
- def compose(layout_path: Path, cell_paths: list[Path],
28
- title_lines: list[str], caption_lines: list[str],
29
- output_path: Path) -> Path:
30
- """根据 layout.json 渲染海报。
31
-
32
- layout.json 字段:
33
- canvas_size [w, h]
34
- cells.positions [[x,y], ...] cell 左上角
35
- cells.sizes [[w,h], ...] cell 尺寸
36
- text_zones.title {lines: [...], size, color, stroke_w, stroke_color, font}
37
- text_zones.caption {同上}
38
- """
39
- layout = json.loads(layout_path.read_text(encoding="utf-8"))
40
- # 约定:layout 必须放在 <skill>/layouts/ 下,字体路径相对 <skill>/ 解析
41
- skill_dir = layout_path.parent.parent
42
-
43
- # 文本行数 vs layout 配置行数:行多了 zip 会静默截断,显式 fail-fast
44
- # 错误信息用英文 — sys.exit 走 stderr,Windows GBK locale 下中文会乱码
45
- for zone_key, lines in [("title", title_lines), ("caption", caption_lines)]:
46
- zone = layout.get("text_zones", {}).get(zone_key)
47
- if not zone:
48
- continue
49
- max_lines = len(zone["lines"])
50
- if len(lines) > max_lines:
51
- sys.exit(f"too many {zone_key} lines: got {len(lines)}, layout supports {max_lines}")
52
-
53
- # Canvas
54
- canvas = Image.new("RGB", tuple(layout["canvas_size"]), "white")
55
-
56
- # Cells
57
- cell_count = len(layout["cells"]["positions"])
58
- if len(cell_paths) != cell_count:
59
- sys.exit(f"cell count mismatch: layout expects {cell_count}, got {len(cell_paths)}")
60
- for cp, pos, size in zip(cell_paths,
61
- layout["cells"]["positions"],
62
- layout["cells"]["sizes"]):
63
- img = Image.open(cp).convert("RGB").resize(tuple(size), Image.LANCZOS)
64
- canvas.paste(img, tuple(pos))
65
-
66
- draw = ImageDraw.Draw(canvas)
67
-
68
- # Text zones (title + caption 通用绘制)
69
- for zone_key, lines in [("title", title_lines), ("caption", caption_lines)]:
70
- if zone_key not in layout["text_zones"]:
71
- continue
72
- zone = layout["text_zones"][zone_key]
73
- font_path = skill_dir / zone["font"]
74
- font = ImageFont.truetype(str(font_path), zone["size"])
75
- for text, line_cfg in zip(lines, zone["lines"]):
76
- if not text:
77
- continue
78
- draw.text(
79
- tuple(line_cfg["position"]), text,
80
- fill=zone["color"], font=font,
81
- stroke_width=zone.get("stroke_w", 0),
82
- stroke_fill=zone.get("stroke_color", zone["color"]),
83
- anchor=line_cfg.get("anchor", "la"),
84
- )
85
-
86
- canvas.save(output_path, optimize=True)
87
- return output_path
88
-
89
-
90
- def main():
91
- ap = argparse.ArgumentParser(description=__doc__,
92
- formatter_class=argparse.RawDescriptionHelpFormatter)
93
- ap.add_argument("--layout", required=True, type=Path,
94
- help="layout.json 路径(如 SKILL_DIR/layouts/2x2.json)")
95
- ap.add_argument("--cells", required=True, nargs="+", type=Path,
96
- help="N 张 cell 图片路径,顺序对应 layout.cells.positions")
97
- ap.add_argument("--title-line", action="append", default=[],
98
- help="标题行(可重复)")
99
- ap.add_argument("--caption-line", action="append", default=[],
100
- help="底部 caption 行(可重复)")
101
- ap.add_argument("--output", required=True, type=Path,
102
- help="输出 PNG 路径")
103
- args = ap.parse_args()
104
-
105
- out = compose(
106
- layout_path=args.layout,
107
- cell_paths=args.cells,
108
- title_lines=args.title_line,
109
- caption_lines=args.caption_line,
110
- output_path=args.output,
111
- )
112
- print(f"saved {out} ({out.stat().st_size // 1024} KB)")
113
-
114
-
115
- if __name__ == "__main__":
116
- main()
1
+ #!/usr/bin/env python3
2
+ """multigrid-poster compose — Pillow 版渲染器
3
+
4
+ 输入:layout.json + N 张 cell 图片 + 标题文字 + caption 文字
5
+ 输出:1242×1660 PNG(小红书封面标准)
6
+
7
+ 用法:
8
+ python compose.py \
9
+ --layout <SKILL_DIR>/layouts/2x2.json \
10
+ --cells cell_0.png cell_1.png cell_2.png cell_3.png \
11
+ --title-line "26岁一个人创业" \
12
+ --title-line "跨境电商月入10w+" \
13
+ --caption-line "只需要一部手机就可以完成!" \
14
+ --caption-line "跨境人的必备app推荐" \
15
+ --output cover.png
16
+
17
+ 依赖:Pillow (容器自带,无需额外安装)
18
+ 字体:从 SKILL_DIR/shared/fonts/ 加载(layout.json 里指定)
19
+ """
20
+ import argparse
21
+ import json
22
+ import sys
23
+ from pathlib import Path
24
+ from PIL import Image, ImageDraw, ImageFont
25
+
26
+
27
+ def compose(layout_path: Path, cell_paths: list[Path],
28
+ title_lines: list[str], caption_lines: list[str],
29
+ output_path: Path) -> Path:
30
+ """根据 layout.json 渲染海报。
31
+
32
+ layout.json 字段:
33
+ canvas_size [w, h]
34
+ cells.positions [[x,y], ...] cell 左上角
35
+ cells.sizes [[w,h], ...] cell 尺寸
36
+ text_zones.title {lines: [...], size, color, stroke_w, stroke_color, font}
37
+ text_zones.caption {同上}
38
+ """
39
+ layout = json.loads(layout_path.read_text(encoding="utf-8"))
40
+ # 约定:layout 必须放在 <skill>/layouts/ 下,字体路径相对 <skill>/ 解析
41
+ skill_dir = layout_path.parent.parent
42
+
43
+ # 文本行数 vs layout 配置行数:行多了 zip 会静默截断,显式 fail-fast
44
+ # 错误信息用英文 — sys.exit 走 stderr,Windows GBK locale 下中文会乱码
45
+ for zone_key, lines in [("title", title_lines), ("caption", caption_lines)]:
46
+ zone = layout.get("text_zones", {}).get(zone_key)
47
+ if not zone:
48
+ continue
49
+ max_lines = len(zone["lines"])
50
+ if len(lines) > max_lines:
51
+ sys.exit(f"too many {zone_key} lines: got {len(lines)}, layout supports {max_lines}")
52
+
53
+ # Canvas
54
+ canvas = Image.new("RGB", tuple(layout["canvas_size"]), "white")
55
+
56
+ # Cells
57
+ cell_count = len(layout["cells"]["positions"])
58
+ if len(cell_paths) != cell_count:
59
+ sys.exit(f"cell count mismatch: layout expects {cell_count}, got {len(cell_paths)}")
60
+ for cp, pos, size in zip(cell_paths,
61
+ layout["cells"]["positions"],
62
+ layout["cells"]["sizes"]):
63
+ img = Image.open(cp).convert("RGB").resize(tuple(size), Image.LANCZOS)
64
+ canvas.paste(img, tuple(pos))
65
+
66
+ draw = ImageDraw.Draw(canvas)
67
+
68
+ # Text zones (title + caption 通用绘制)
69
+ for zone_key, lines in [("title", title_lines), ("caption", caption_lines)]:
70
+ if zone_key not in layout["text_zones"]:
71
+ continue
72
+ zone = layout["text_zones"][zone_key]
73
+ font_path = skill_dir / zone["font"]
74
+ font = ImageFont.truetype(str(font_path), zone["size"])
75
+ for text, line_cfg in zip(lines, zone["lines"]):
76
+ if not text:
77
+ continue
78
+ draw.text(
79
+ tuple(line_cfg["position"]), text,
80
+ fill=zone["color"], font=font,
81
+ stroke_width=zone.get("stroke_w", 0),
82
+ stroke_fill=zone.get("stroke_color", zone["color"]),
83
+ anchor=line_cfg.get("anchor", "la"),
84
+ )
85
+
86
+ canvas.save(output_path, optimize=True)
87
+ return output_path
88
+
89
+
90
+ def main():
91
+ ap = argparse.ArgumentParser(description=__doc__,
92
+ formatter_class=argparse.RawDescriptionHelpFormatter)
93
+ ap.add_argument("--layout", required=True, type=Path,
94
+ help="layout.json 路径(如 SKILL_DIR/layouts/2x2.json)")
95
+ ap.add_argument("--cells", required=True, nargs="+", type=Path,
96
+ help="N 张 cell 图片路径,顺序对应 layout.cells.positions")
97
+ ap.add_argument("--title-line", action="append", default=[],
98
+ help="标题行(可重复)")
99
+ ap.add_argument("--caption-line", action="append", default=[],
100
+ help="底部 caption 行(可重复)")
101
+ ap.add_argument("--output", required=True, type=Path,
102
+ help="输出 PNG 路径")
103
+ args = ap.parse_args()
104
+
105
+ out = compose(
106
+ layout_path=args.layout,
107
+ cell_paths=args.cells,
108
+ title_lines=args.title_line,
109
+ caption_lines=args.caption_line,
110
+ output_path=args.output,
111
+ )
112
+ print(f"saved {out} ({out.stat().st_size // 1024} KB)")
113
+
114
+
115
+ if __name__ == "__main__":
116
+ main()