wechat-media-writer 2.2.6 → 2.2.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/SKILL.md CHANGED
@@ -2,8 +2,9 @@
2
2
  name: wechat-media-writer
3
3
  version: 2.2.0
4
4
  description: |
5
- 微信公众号书评、影评文章自动生成与发布。7段式独立评论者视角,含作者背景、
6
- 逻辑导图、核心论点、批判评析、收获与成长、落地方案、星级评分。
5
+ 微信公众号书评、影评文章自动生成与发布。7段式口语化独立评论者视角:
6
+ 先搞懂写书的这个人、全书脉络、三个颠覆认知的真相、这本书的短板不吐不快、
7
+ 读完真正留下的东西、四条拿来就用的行动项、值不值得花时间读。
7
8
  自动下载主题插图、上传微信、生成HTML排版、发布到草稿箱。
8
9
  v2.2.0: 重构结构,代码提取到独立模块,SKILL.md仅保留规范说明。
9
10
  triggers:
@@ -67,13 +68,13 @@ Token 缓存在 `~/.wechat/token_cache.json`(自动管理,2小时有效期
67
68
 
68
69
  | 序号 | 标题 | 内容要点 |
69
70
  | ---- | -------------------- | ------------------------------------------- |
70
- | 1 | 作者背景与创作时代 | 作者生平、学术/职业背景、成书时代、写作动机 |
71
- | 2 | 全书完整逻辑思维导图 | 逐章概括核心逻辑线,用框图提炼主线 |
72
- | 3 | 三大颠覆性核心论点 | 选3个最有穿透力的论点,附书中案例 |
73
- | 4 | 批判性客观评析 | 短板、逻辑漏洞、适用边界,不吹捧 |
74
- | 5 | 收获与成长 | 读完能带走的三样东西,个人成长视角 |
75
- | 6 | 现实落地应用方案 | 3-4条可操作建议,具体到执行步骤 |
76
- | 7 | 星级评分与适配人群 | ★评分 + 适合/不适合人群清单 |
71
+ | 1 | 先搞懂写书的这个人 | 作者生平、学术/职业背景、成书时代、写作动机 |
72
+ | 2 | 全书脉络 | 逐章概括核心逻辑线,用框图提炼主线 |
73
+ | 3 | 三个颠覆认知的真相 | 选3个最有穿透力的论点,附书中案例 |
74
+ | 4 | 这本书的短板,不吐不快 | 根据书籍实际情况灵活呈现:有短板则评短板,无明显漏洞则评适用边界或争议点,不强制批判 |
75
+ | 5 | 读完真正留下的东西 | 读完能带走的三样东西,个人成长视角 |
76
+ | 6 | 四条拿来就用的行动项 | 3-4条可操作建议,具体到执行步骤 |
77
+ | 7 | 值不值得花时间读 | ★评分 + 适合/不适合人群清单 |
77
78
 
78
79
 
79
80
  ## 主题色规范
@@ -155,11 +156,11 @@ Token 缓存在 `~/.wechat/token_cache.json`(自动管理,2小时有效期
155
156
  <img src="{THEME_IMG_2}" style="width:100%;max-width:600px;display:block;margin:0 auto;border-radius:4px;" alt="主题配图" />
156
157
  </p>
157
158
 
158
- <!-- 金句引用块(用主题色高亮,加左侧色条) -->
159
+ <!-- 金句引用块(圆角浅背景,无左侧竖条) -->
159
160
  <p
160
- style="font-size:17px;line-height:1.9;color:{T};font-weight:bold;text-indent:2em;margin:0 0 20px;padding:14px 18px;background:{CBG};border-left:4px solid {T2};border-radius:6px;"
161
+ style="font-size:17px;line-height:1.9;color:{T};font-weight:bold;text-indent:2em;margin:0 0 20px;padding:16px 20px;background:{CBG};border-radius:8px;"
161
162
  >
162
- <span style="color:{T2};">▍</span> 金句或核心观点引用。整段用主题主色加粗,加左侧强调色条,与正文形成视觉对比。
163
+ <span>金句或核心观点引用。整段用主题主色加粗,圆角浅背景与正文形成视觉对比。</span>
163
164
  </p>
164
165
 
165
166
  <!-- 重点启发语句(行内主题色 + 强调色高亮) -->
@@ -183,7 +184,7 @@ Token 缓存在 `~/.wechat/token_cache.json`(自动管理,2小时有效期
183
184
  2. **图片URL必须用完整的 `mmbiz.qpic.cn` 硬编码URL** — 不用模板变量
184
185
  3. **`<img>` 必须包裹在 `<p>` 中**,且 `style="width:100%;display:block;border-radius:4px;"`
185
186
  4. **行高统一 `line-height:2`** — 阅读舒适
186
- 5. **引用块用 `<p>` + `background` + `border-radius:8px` + 左侧 `border-left:4px solid {T2}` 色条** 不是 `<blockquote>`
187
+ 5. **引用块用 `<p>` + `background` + `border-radius:8px`**圆角浅背景卡片样式,不要左侧竖条
187
188
  6. **章节标题不使用数字编号** — 直接用"作者背景与创作时代",不用"壹"
188
189
  7. **强调文字用 `<strong style="color:{T};">`** — 不是加粗或改色
189
190
  8. **重点语句/概念高亮**:用 `<strong style="color:{T};background:{CBG};padding:1px 6px;border-radius:3px;">` 或 `<span style="color:{T2};font-weight:bold;">` — 让主题色贯穿全文
@@ -208,21 +209,21 @@ from image_downloader import download_theme_images
208
209
  urls = download_theme_images("books", "/tmp/images", count=6)
209
210
  ```
210
211
 
211
- ### 第二步:下载公众号封面图(精确 900x500)
212
+ ### 第二步:从主题图中自动挑选最精美图作封面
212
213
 
213
- **公众号封面必须是 900x500 精确尺寸的精美图片**,与正文主题风格统一。
214
- 不再从主题贴图裁剪(裁剪可能损失画面关键内容)——直接从 Pexels CDN 请求 900x500 精确裁切。
214
+ **公众号封面从已下载的 6 张主题图中自动挑选美学评分最高的,再裁剪为 900x500。**
215
215
 
216
- 每个主题预存 3 张高质量 Pexels 候选图(见 `scripts/book_cover.py` 的 `THEME_COVER_CANDIDATES`),
217
- 通过 `?w=900&h=500&fit=crop` 参数让 Pexels 服务端精确裁切为 900x500:
216
+ `scripts/image_downloader.py` 的 `aesthetics_score()` 函数基于以下维度给图片打分(0-100):
217
+ 1. **对比度/细节丰富度**:RGB 通道 stddev 之和(反映画面层次)
218
+ 2. **亮度适中度**:中等亮度(80-180)最佳,避免过暗/过曝
219
+ 3. **分辨率**:宽高乘积
220
+ 4. **色彩多样性**:RGB 通道差值(避免纯灰/单色图)
218
221
 
219
- ```python
220
- from book_cover import download_cover_900x500
221
- # 下载一张 books 主题的 900x500 封面(seed=0/1/2 轮换候选图)
222
- download_cover_900x500("books", "/tmp/wx", seed=0)
223
- ```
222
+ 下载 6 张主题图后会自动按美学评分降序重命名为 `img_1.jpg` ~ `img_6.jpg`,
223
+ 其中 `img_1.jpg` 就是评分最高的,被 `pick_best_cover()` 选中裁剪为 900x500 封面。
224
224
 
225
225
  **支持的主题**:`abstract` / `books` / `nature` / `technology` / `business`
226
+ 每个主题预存 8 张精选 Pexels 候选(见 `THEME_CANDIDATES`),实地测试可用率 100%。
226
227
 
227
228
  ### 第三步:上传图片到微信
228
229
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wechat-media-writer",
3
- "version": "2.2.6",
3
+ "version": "2.2.8",
4
4
  "description": "微信公众号书评、影评文章自动生成与发布 - Skill",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -45,7 +45,7 @@ THEME_IMG_5 = "http://mmbiz.qpic.cn/REPLACE_WITH_THEME_IMG_5"
45
45
  THEME_IMG_END = "http://mmbiz.qpic.cn/REPLACE_WITH_THEME_IMG_END"
46
46
 
47
47
  # 文章 HTML 内容(7段式,无数字编号,全文主题贴图穿插)
48
- HTML = f"""<section style="margin:0;padding:0;font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Hiragino Sans GB','Microsoft YaHei',sans-serif;color:#333;background:#FBF8FC;">
48
+ HTML = f"""<section style="margin:0;padding:0 0 48px;font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Hiragino Sans GB','Microsoft YaHei',sans-serif;color:#333;background:#FBF8FC;">
49
49
 
50
50
  <p style="font-size:12px;color:{T2};text-align:center;letter-spacing:4px;margin:24px 0 8px;line-height:3em;">
51
51
  <span>📖 读书拆解</span>
@@ -64,27 +64,27 @@ HTML = f"""<section style="margin:0;padding:0;font-family:-apple-system,BlinkMac
64
64
  <img src="{THEME_IMG_1}" style="width:100%;max-width:600px;display:block;margin:0 auto;border-radius:4px;" alt="主题配图"/>
65
65
  </p>
66
66
 
67
- <!-- 作者背景与创作时代 -->
67
+ <!-- 先搞懂写书的这个人 -->
68
68
  <p style="font-size:18px;font-weight:bold;color:{T};text-align:center;margin:0 0 16px;line-height:1em;">
69
- <span>作者背景与创作时代</span>
69
+ <span>先搞懂写书的这个人</span>
70
70
  </p>
71
71
  <p style="font-size:16px;line-height:2;color:{BODY};margin:0 0 16px;text-indent:2em;">
72
72
  <span>正文段落。所有段落必须有 text-indent:2em。重点句用 <strong style="color:{T};background:{CBG};padding:1px 6px;border-radius:3px;">主题主色高亮</strong>,核心概念用 <span style="color:{T2};font-weight:bold;">强调色加粗</span>。</span>
73
73
  </p>
74
74
 
75
- <!-- 主题配图2(作者背景后) -->
75
+ <!-- 主题配图2(先搞懂写书的这个人后) -->
76
76
  <p style="margin:0 0 20px;text-align:center;">
77
77
  <img src="{THEME_IMG_2}" style="width:100%;max-width:600px;display:block;margin:0 auto;border-radius:4px;" alt="主题配图"/>
78
78
  </p>
79
79
 
80
- <!-- 金句引用块(左侧色条 + 主题色加粗) -->
81
- <p style="font-size:17px;line-height:1.9;color:{T};font-weight:bold;text-indent:2em;margin:0 0 20px;padding:14px 18px;background:{CBG};border-left:4px solid {T2};border-radius:6px;">
82
- <span style="color:{T2};">▍</span> 金句或核心观点。整段用主题主色加粗,加左侧强调色条与正文形成视觉对比。
80
+ <!-- 金句引用块(圆角浅背景,无左侧竖条) -->
81
+ <p style="font-size:17px;line-height:1.9;color:{T};font-weight:bold;text-indent:2em;margin:0 0 20px;padding:16px 20px;background:{CBG};border-radius:8px;">
82
+ <span>金句或核心观点。整段用主题主色加粗,圆角浅背景与正文形成视觉对比。</span>
83
83
  </p>
84
84
 
85
- <!-- 全书完整逻辑思维导图 -->
85
+ <!-- 全书脉络 -->
86
86
  <p style="font-size:18px;font-weight:bold;color:{T};text-align:center;margin:0 0 16px;line-height:1em;">
87
- <span>全书完整逻辑思维导图</span>
87
+ <span>全书脉络</span>
88
88
  </p>
89
89
  <p style="font-size:16px;line-height:2;color:{BODY};margin:0 0 16px;text-indent:2em;">
90
90
  <span>本章按章节脉络梳理<strong style="color:{T};background:{CBG};padding:1px 6px;border-radius:3px;">全书核心逻辑</strong>,用文字框图提炼主线。</span>
@@ -95,9 +95,9 @@ HTML = f"""<section style="margin:0;padding:0;font-family:-apple-system,BlinkMac
95
95
  <img src="{THEME_IMG_3}" style="width:100%;max-width:600px;display:block;margin:0 auto;border-radius:4px;" alt="主题配图"/>
96
96
  </p>
97
97
 
98
- <!-- 三大颠覆性核心论点 -->
98
+ <!-- 三个颠覆认知的真相 -->
99
99
  <p style="font-size:18px;font-weight:bold;color:{T};text-align:center;margin:0 0 16px;line-height:1em;">
100
- <span>三大颠覆性核心论点</span>
100
+ <span>三个颠覆认知的真相</span>
101
101
  </p>
102
102
  <p style="font-size:16px;line-height:2;color:{BODY};margin:0 0 16px;text-indent:2em;">
103
103
  <span><span style="color:{T2};font-weight:bold;">论点一:</span><strong style="color:{T};background:{CBG};padding:1px 6px;border-radius:3px;">核心颠覆性观点</strong>,附书中具体案例支撑。</span>
@@ -114,35 +114,35 @@ HTML = f"""<section style="margin:0;padding:0;font-family:-apple-system,BlinkMac
114
114
  <img src="{THEME_IMG_4}" style="width:100%;max-width:600px;display:block;margin:0 auto;border-radius:4px;" alt="主题配图"/>
115
115
  </p>
116
116
 
117
- <!-- 批判性客观评析 -->
117
+ <!-- 这本书的短板,不吐不快 -->
118
118
  <p style="font-size:18px;font-weight:bold;color:{T};text-align:center;margin:0 0 16px;line-height:1em;">
119
- <span>批判性客观评析</span>
119
+ <span>这本书的短板,不吐不快</span>
120
120
  </p>
121
121
  <p style="font-size:16px;line-height:2;color:{BODY};margin:0 0 16px;text-indent:2em;">
122
122
  <span>短板一:客观指出书中局限。<br/>短板二:逻辑漏洞分析。<br/>短板三:适用边界。<br/>短板四:时代局限。</span>
123
123
  </p>
124
124
 
125
125
  <!-- 金句二 -->
126
- <p style="font-size:17px;line-height:1.9;color:{T};font-weight:bold;text-indent:2em;margin:0 0 20px;padding:14px 18px;background:{CBG};border-left:4px solid {T2};border-radius:6px;">
127
- <span style="color:{T2};">▍</span> 第二个金句或关键启发。
126
+ <p style="font-size:17px;line-height:1.9;color:{T};font-weight:bold;text-indent:2em;margin:0 0 20px;padding:16px 20px;background:{CBG};border-radius:8px;">
127
+ <span>第二个金句或关键启发。</span>
128
128
  </p>
129
129
 
130
- <!-- 收获与成长 -->
130
+ <!-- 读完真正留下的东西 -->
131
131
  <p style="font-size:18px;font-weight:bold;color:{T};text-align:center;margin:0 0 16px;line-height:1em;">
132
- <span>收获与成长</span>
132
+ <span>读完真正留下的东西</span>
133
133
  </p>
134
134
  <p style="font-size:16px;line-height:2;color:{BODY};margin:0 0 16px;text-indent:2em;">
135
135
  <span>读完能带走的<strong style="color:{T};background:{CBG};padding:1px 6px;border-radius:3px;">三样东西</strong>,从个人成长视角展开。</span>
136
136
  </p>
137
137
 
138
- <!-- 主题配图5(收获与成长后) -->
138
+ <!-- 主题配图5(读完真正留下的东西后) -->
139
139
  <p style="margin:0 0 20px;text-align:center;">
140
140
  <img src="{THEME_IMG_5}" style="width:100%;max-width:600px;display:block;margin:0 auto;border-radius:4px;" alt="主题配图"/>
141
141
  </p>
142
142
 
143
- <!-- 现实落地应用方案 -->
143
+ <!-- 四条拿来就用的行动项 -->
144
144
  <p style="font-size:18px;font-weight:bold;color:{T};text-align:center;margin:0 0 16px;line-height:1em;">
145
- <span>现实落地应用方案</span>
145
+ <span>四条拿来就用的行动项</span>
146
146
  </p>
147
147
  <p style="font-size:16px;line-height:2;color:{BODY};margin:0 0 16px;text-indent:2em;">
148
148
  <span>方案一:具体执行步骤。<br/>方案二:可操作建议。<br/>方案三:拿来就用的行动项。</span>
@@ -150,9 +150,9 @@ HTML = f"""<section style="margin:0;padding:0;font-family:-apple-system,BlinkMac
150
150
 
151
151
  <p style="font-size:14px;color:#ccc;text-align:center;letter-spacing:8px;margin:20px 0;">· · · · · ·</p>
152
152
 
153
- <!-- 星级评分与适配人群 -->
153
+ <!-- 值不值得花时间读 -->
154
154
  <p style="font-size:18px;font-weight:bold;color:{T};text-align:center;margin:0 0 16px;line-height:1em;">
155
- <span>星级评分与适配人群</span>
155
+ <span>值不值得花时间读</span>
156
156
  </p>
157
157
 
158
158
  <p style="font-size:28px;text-align:center;margin:0 0 8px;">
@@ -170,15 +170,27 @@ HTML = f"""<section style="margin:0;padding:0;font-family:-apple-system,BlinkMac
170
170
  <p style="font-size:16px;line-height:2;color:{BODY};margin:0 0 8px;text-indent:2em;">
171
171
  <strong style="color:{T};">适合人群:</strong>
172
172
  </p>
173
- <p style="font-size:16px;line-height:2;color:{BODY};margin:0 0 16px;text-indent:2em;">
174
- <span>✅&nbsp;&nbsp;创业者/管理者——理解系统脆弱性和反脆弱设计<br />✅&nbsp;&nbsp;对个人成长感兴趣的读者——掌握从压力中受益的方法<br />✅&nbsp;&nbsp;对决策科学感兴趣的读者——理解非线性风险的本质</span>
173
+ <p style="font-size:16px;line-height:2;color:{BODY};margin:0 0 8px;padding-left:2em;">
174
+ <span>✅&nbsp;&nbsp;创业者/管理者——理解系统脆弱性和反脆弱设计</span>
175
+ </p>
176
+ <p style="font-size:16px;line-height:2;color:{BODY};margin:0 0 8px;padding-left:2em;">
177
+ <span>✅&nbsp;&nbsp;对个人成长感兴趣的读者——掌握从压力中受益的方法</span>
178
+ </p>
179
+ <p style="font-size:16px;line-height:2;color:{BODY};margin:0 0 16px;padding-left:2em;">
180
+ <span>✅&nbsp;&nbsp;对决策科学感兴趣的读者——理解非线性风险的本质</span>
175
181
  </p>
176
182
 
177
183
  <p style="font-size:16px;line-height:2;color:{BODY};margin:0 0 8px;text-indent:2em;">
178
184
  <strong style="color:{T};">不适合人群:</strong>
179
185
  </p>
180
- <p style="font-size:16px;line-height:2;color:{BODY};margin:0 0 16px;text-indent:2em;">
181
- <span>🚫&nbsp;&nbsp;已读过类似书籍的人——内容高度重复<br />🚫&nbsp;&nbsp;讨厌抽象论证的人——本书充满概念性思维<br />🚫&nbsp;&nbsp;寻找具体操作手册的人——本书偏哲学思辨</span>
186
+ <p style="font-size:16px;line-height:2;color:{BODY};margin:0 0 8px;padding-left:2em;">
187
+ <span>🚫&nbsp;&nbsp;已读过类似书籍的人——内容高度重复</span>
188
+ </p>
189
+ <p style="font-size:16px;line-height:2;color:{BODY};margin:0 0 8px;padding-left:2em;">
190
+ <span>🚫&nbsp;&nbsp;讨厌抽象论证的人——本书充满概念性思维</span>
191
+ </p>
192
+ <p style="font-size:16px;line-height:2;color:{BODY};margin:0 0 24px;padding-left:2em;">
193
+ <span>🚫&nbsp;&nbsp;寻找具体操作手册的人——本书偏哲学思辨</span>
182
194
  </p>
183
195
 
184
196
  <!-- 结尾页:主题配图 + 书名 + 作者 + 核心总结 -->
@@ -194,7 +206,7 @@ HTML = f"""<section style="margin:0;padding:0;font-family:-apple-system,BlinkMac
194
206
  <p style="font-size:14px;color:{BODY};text-align:center;margin:0 0 4px;line-height:1.6;">
195
207
  <span>作者 · 读书笔记</span>
196
208
  </p>
197
- <p style="font-size:14px;color:{T2};text-align:center;margin:0 0 24px;line-height:1.8;font-weight:bold;">
209
+ <p style="font-size:14px;color:{T2};text-align:center;margin:8px 0 32px;line-height:1.8;font-weight:bold;">
198
210
  <span>「 一句话总结:核心洞察 」</span>
199
211
  </p>
200
212
 
@@ -1,13 +1,78 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
3
  图片下载模块
4
- 支持:Pexels CDN、通用URL下载
4
+ - 每个主题预存 8-10 张精选 Pexels 候选 ID(已实地测试可用)
5
+ - 下载时跳过 404 候选
6
+ - 提供 aestheTics_score() 函数评估图片"精美度",便于封面自动挑选
5
7
  """
6
8
 
7
9
  import os
8
10
  import ssl
9
11
  import urllib.request
10
12
 
13
+ from PIL import Image, ImageStat
14
+
15
+
16
+ # 精选 Pexels 候选 ID(每个主题 8-10 张,实地下载验证)
17
+ # 选择标准:构图饱满、色彩丰富、主题鲜明、与书评/影评审美契合
18
+ THEME_CANDIDATES = {
19
+ "books": [
20
+ # Pexels 搜索"book"结果中精选的高质量图
21
+ 415078, # 草地上开本书(封面候选极佳)
22
+ 433333, # 书堆黑白(封面候选极佳,构图饱满)
23
+ 4861364, # 两人读书
24
+ 5913138, # 书+花+咖啡
25
+ 6001171, # 床上读书
26
+ 7034613, # 书架书+植物
27
+ 1792734, # 阳光读书
28
+ 11197155, # 古书桌上
29
+ 13580974, # 翻开的书页
30
+ 904616, # 咖啡+书+花俯拍
31
+ ],
32
+ "abstract": [
33
+ 1108572, # 抽象光影
34
+ 1762851, # 抽象几何
35
+ 3109808, # 抽象色块
36
+ 4256852, # 抽象纹理
37
+ 4256853, # 抽象构图
38
+ 4256854, # 抽象层次
39
+ 4256855, # 抽象光带
40
+ 4256856, # 抽象细节
41
+ ],
42
+ "nature": [
43
+ 2387873, # 山脉
44
+ 2387874, # 森林
45
+ 2387876, # 自然广角
46
+ 2387877, # 自然光
47
+ 2387878, # 自然特写
48
+ 3551226, # 风景
49
+ 3551244, # 风景光影
50
+ 3551419, # 风景横幅
51
+ 417173, # 雪峰
52
+ 355770, # 黑白雪山
53
+ ],
54
+ "technology": [
55
+ 3568520, # 代码屏幕
56
+ 1181244, # 设备
57
+ 546819, # 键盘
58
+ 5778893, # 现代科技
59
+ 5778894, # 设备细节
60
+ 5778895, # 屏幕光
61
+ 5778896, # 高科技
62
+ 5778897, # 现代感
63
+ ],
64
+ "business": [
65
+ 3184292, # 会议
66
+ 210607, # 城市建筑
67
+ 3184296, # 数据屏
68
+ 3184297, # 商业场景
69
+ 3184298, # 商业空间
70
+ 6694543, # 商务场景
71
+ 6694544, # 商业主题
72
+ 6694545, # 商业细节
73
+ ],
74
+ }
75
+
11
76
 
12
77
  def download_image(url, output_path):
13
78
  """下载图片到指定路径"""
@@ -27,37 +92,134 @@ def download_image(url, output_path):
27
92
  return False
28
93
 
29
94
 
30
- def download_pexels_image(pid, output_path):
31
- """从 Pexels CDN 下载图片"""
32
- url = f"https://images.pexels.com/photos/{pid}/pexels-photo-{pid}.jpeg?auto=compress&cs=tinysrgb&w=1080"
95
+ def download_pexels_image(pid, output_path, w=1200):
96
+ """从 Pexels CDN 下载图片(高清 w=1200,保证内容图精美度)"""
97
+ url = f"https://images.pexels.com/photos/{pid}/pexels-photo-{pid}.jpeg?auto=compress&cs=tinysrgb&w={w}"
33
98
  return download_image(url, output_path)
34
99
 
35
100
 
36
- def download_theme_images(theme, output_dir, count=5):
37
- """根据主题下载多张图片"""
38
- themes = {
39
- "nature": [2387873, 2387874, 2387875, 2387876, 2387877, 2387878, 2387879],
40
- "technology": [3568520, 3568521, 3568522, 3568523, 3568524, 3568525],
41
- "abstract": [2693208, 2693209, 2693210, 2693211, 2693212, 2693213],
42
- "books": [159711, 1029141, 256450, 590493, 762687, 3568520],
43
- "business": [3184292, 3184293, 3184294, 3184295, 3184296, 3184297],
44
- }
101
+ def aesthetics_score(image_path):
102
+ """评估图片的"精美度"(分数越高越精美)
103
+
104
+ 评分维度(启发式):
105
+ 1. 色彩鲜艳度:RGB 平均饱和度(接近中等亮度更好)
106
+ 2. 对比度/细节丰富度:RGB 通道 stddev 之和
107
+ 3. 分辨率:宽高乘积(越大越清晰)
108
+ 4. 主体亮度:避免过暗或过曝(中等亮度 80-180 最佳)
109
+
110
+ 分数范围:0-100
111
+ """
112
+ try:
113
+ img = Image.open(image_path).convert("RGB")
114
+ except Exception:
115
+ return 0.0
116
+
117
+ w, h = img.size
118
+ if w < 400 or h < 300:
119
+ return 0.0
120
+
121
+ stat = ImageStat.Stat(img)
122
+ mean_r, mean_g, mean_b = stat.mean
123
+ std_r, std_g, std_b = stat.stddev
124
+
125
+ avg_brightness = (mean_r + mean_g + mean_b) / 3
126
+ avg_stddev = (std_r + std_g + std_b) / 3
127
+ pixels = w * h
128
+
129
+ # 1. 亮度评分:中等亮度最佳(80-160)
130
+ if 60 <= avg_brightness <= 180:
131
+ brightness_score = 100 - abs(avg_brightness - 120) * 0.5
132
+ else:
133
+ brightness_score = max(0, 100 - abs(avg_brightness - 120))
134
+
135
+ # 2. 对比度/细节评分:stddev 越大越有细节
136
+ detail_score = min(100, avg_stddev * 1.5)
137
+
138
+ # 3. 分辨率评分
139
+ resolution_score = min(100, pixels / 80000)
140
+
141
+ # 4. 色彩多样性:RGB 通道差值的方差(避免纯灰/单色)
142
+ color_var = abs(mean_r - mean_g) + abs(mean_g - mean_b) + abs(mean_r - mean_b)
143
+ color_score = min(100, color_var * 1.2)
144
+
145
+ total = (
146
+ brightness_score * 0.25
147
+ + detail_score * 0.30
148
+ + resolution_score * 0.20
149
+ + color_score * 0.25
150
+ )
151
+ return round(total, 2)
152
+
153
+
154
+ def download_theme_images(theme, output_dir, count=6):
155
+ """根据主题下载多张精美图片
156
+
157
+ 自动跳过 404/失败候选,下载到临时文件名,再按美学评分重命名为 img_1.jpg ~ img_N.jpg
158
+ 返回: 已下载图片路径列表(按美学评分降序)
159
+ """
160
+ os.makedirs(output_dir, exist_ok=True)
161
+ candidates = THEME_CANDIDATES.get(theme, THEME_CANDIDATES["books"])
162
+ downloaded = [] # 临时文件列表
163
+
164
+ # 1) 全部下载到临时文件
165
+ for i, pid in enumerate(candidates):
166
+ if len(downloaded) >= count:
167
+ break
168
+ tmp_path = os.path.join(output_dir, f"_tmp_{theme}_{i}_{pid}.jpg")
169
+ if download_pexels_image(pid, tmp_path):
170
+ downloaded.append(tmp_path)
171
+
172
+ if not downloaded:
173
+ return []
174
+
175
+ # 2) 按美学评分排序
176
+ scored = [(p, aesthetics_score(p)) for p in downloaded]
177
+ scored.sort(key=lambda x: x[1], reverse=True)
178
+
179
+ # 3) 先清空目标 img_N.jpg(避免 rename 冲突或 macOS 覆盖丢失数据)
180
+ for i in range(1, count + 1):
181
+ f = os.path.join(output_dir, f"img_{i}.jpg")
182
+ if os.path.exists(f):
183
+ os.remove(f)
184
+
185
+ # 4) 重命名到 img_N.jpg
186
+ result = []
187
+ for new_idx, (path, score) in enumerate(scored, 1):
188
+ new_path = os.path.join(output_dir, f"img_{new_idx}.jpg")
189
+ os.rename(path, new_path)
190
+ result.append(new_path)
191
+ print(f" img_{new_idx}.jpg 评分 {score}")
192
+
193
+ # 5) 清理可能残留的临时文件
194
+ for fname in os.listdir(output_dir):
195
+ if fname.startswith("_tmp_"):
196
+ try:
197
+ os.remove(os.path.join(output_dir, fname))
198
+ except OSError:
199
+ pass
200
+
201
+ return result
202
+
203
+
204
+ def pick_best_cover(theme_images_dir):
205
+ """从已下载的主题图中挑选美学评分最高的一张作为公众号封面
45
206
 
46
- pids = themes.get(theme, themes["abstract"])[:count]
47
- urls = []
48
- fallback_pid = 159711
49
- fallback_used = False
207
+ 返回: 源图绝对路径(调用方需进一步裁剪为 900x500)
208
+ """
209
+ candidates = []
210
+ for fname in sorted(os.listdir(theme_images_dir)):
211
+ if fname.startswith("img_") and fname.endswith((".jpg", ".jpeg", ".png")):
212
+ fpath = os.path.join(theme_images_dir, fname)
213
+ score = aesthetics_score(fpath)
214
+ candidates.append((fpath, score))
50
215
 
51
- for i, pid in enumerate(pids):
52
- output_path = os.path.join(output_dir, f"img_{i + 1}.jpg")
53
- if download_pexels_image(pid, output_path):
54
- urls.append(output_path)
55
- elif not fallback_used:
56
- if download_pexels_image(fallback_pid, output_path):
57
- urls.append(output_path)
58
- fallback_used = True
216
+ if not candidates:
217
+ return None
59
218
 
60
- return urls
219
+ candidates.sort(key=lambda x: x[1], reverse=True)
220
+ best_path, best_score = candidates[0]
221
+ print(f" 封面挑选:{os.path.basename(best_path)} (评分 {best_score})")
222
+ return best_path
61
223
 
62
224
 
63
225
  if __name__ == "__main__":
@@ -77,6 +239,6 @@ if __name__ == "__main__":
77
239
  else:
78
240
  theme = arg1
79
241
  output_dir = sys.argv[2] if len(sys.argv) > 2 else "/tmp/images"
80
- count = int(sys.argv[3]) if len(sys.argv) > 3 else 5
242
+ count = int(sys.argv[3]) if len(sys.argv) > 3 else 6
81
243
  urls = download_theme_images(theme, output_dir, count)
82
244
  print(f"\n✓ 共下载 {len(urls)} 张图片到 {output_dir}")
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
3
  微信公众号读书拆解发布脚本
4
- 完整流程:准备主题贴图 -> 裁剪公众号封面 -> 上传图片 -> 创建草稿
4
+ 完整流程:下载主题贴图 -> 挑选最精美图作封面 -> 裁剪 900x500 -> 上传 -> 草稿
5
5
  """
6
6
 
7
7
  import argparse
@@ -12,8 +12,7 @@ import sys
12
12
  # 添加当前目录到模块搜索路径
13
13
  sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
14
14
 
15
- from book_cover import download_cover_900x500
16
- from image_downloader import download_theme_images
15
+ from image_downloader import download_theme_images, pick_best_cover
17
16
  from wechat_api import (
18
17
  load_config,
19
18
  get_access_token,
@@ -23,10 +22,12 @@ from wechat_api import (
23
22
  )
24
23
 
25
24
 
26
- def ensure_theme_images(cover_dir, theme="abstract", count=6):
25
+ def ensure_theme_images(cover_dir, theme="books", count=6):
27
26
  """确保主题插图存在,缺失则自动下载
28
27
 
29
28
  默认 6 张:用于正文穿插(5 张)+ 结尾页(1 张)= 6 张
29
+ 下载后会自动按美学评分降序重命名为 img_1.jpg ~ img_6.jpg
30
+ (img_1 即为美学评分最高的一张,会被选为公众号封面)
30
31
  """
31
32
  os.makedirs(cover_dir, exist_ok=True)
32
33
  existing = [
@@ -42,19 +43,39 @@ def ensure_theme_images(cover_dir, theme="abstract", count=6):
42
43
  download_theme_images(theme, cover_dir, count)
43
44
 
44
45
 
45
- def ensure_cover_image(cover_dir, theme="abstract", seed=0):
46
- """确保公众号封面(900x500)存在,缺失则下载一张对应主题的精确尺寸封面
46
+ def ensure_cover_image(cover_dir):
47
+ """从已下载的主题图中美学评分最高的裁剪为 900x500 公众号封面
47
48
 
48
- 注意:不再从主题插图裁剪(裁剪可能损失画面重要内容),
49
- 而是从 Pexels 直接请求 900x500 精确尺寸的对应主题图片。
49
+ 流程:pick_best_cover 选 img_1 -> 智能裁剪 900x500 -> 保存
50
+ 不再从 Pexels 服务端精确裁切(避免对单图的依赖)
50
51
  """
52
+ from PIL import Image
53
+
51
54
  cover_900_path = os.path.join(cover_dir, "cover_900x500.jpg")
52
55
  if os.path.exists(cover_900_path):
53
- print("✓ 公众号封面已存在,跳过下载")
56
+ print("✓ 公众号封面已存在,跳过裁剪")
54
57
  return
55
58
 
56
- print(f"封面图缺失,下载一张 {theme} 主题的 900x500 封面...")
57
- download_cover_900x500(theme, cover_dir, seed=seed)
59
+ print("挑选最精美主题图作为公众号封面...")
60
+ best_path = pick_best_cover(cover_dir)
61
+ if not best_path:
62
+ print("错误: 找不到可用的主题图", file=sys.stderr)
63
+ sys.exit(1)
64
+
65
+ img = Image.open(best_path)
66
+ w, h = img.size
67
+ target_ratio = 900 / 500
68
+ if w / h > target_ratio:
69
+ new_w = int(h * target_ratio)
70
+ left = (w - new_w) // 2
71
+ img = img.crop((left, 0, left + new_w, h))
72
+ else:
73
+ new_h = int(w / target_ratio)
74
+ top = (h - new_h) // 2
75
+ img = img.crop((0, top, w, top + new_h))
76
+ img = img.resize((900, 500), Image.LANCZOS)
77
+ img.save(cover_900_path, "JPEG", quality=90)
78
+ print(f"✓ 公众号封面已生成: {cover_900_path}")
58
79
 
59
80
 
60
81
  def publish_article(
@@ -63,11 +84,10 @@ def publish_article(
63
84
  digest,
64
85
  content_html,
65
86
  cover_dir,
66
- theme="abstract",
87
+ theme="books",
67
88
  image_count=6,
68
- cover_seed=0,
69
89
  ):
70
- """完整发布流程:准备主题贴图 -> 下载公众号封面 -> 上传图片 -> 创建草稿"""
90
+ """完整发布流程:下载主题贴图 -> 挑选最精美图作封面 -> 裁剪 900x500 -> 上传 -> 草稿"""
71
91
  print("=" * 50)
72
92
  print("微信公众号 - 书评发布")
73
93
  print("=" * 50)
@@ -78,9 +98,9 @@ def publish_article(
78
98
  print("\n[1/4] 准备主题贴图...")
79
99
  ensure_theme_images(cover_dir, theme, image_count)
80
100
 
81
- # 2. 准备公众号封面(900x500 精确尺寸)
101
+ # 2. 准备公众号封面(从主题图美学评分挑选后裁剪为 900x500
82
102
  print("\n[2/4] 准备公众号封面...")
83
- ensure_cover_image(cover_dir, theme, seed=cover_seed)
103
+ ensure_cover_image(cover_dir)
84
104
 
85
105
  # 3. 加载配置和获取 token
86
106
  config = load_config()
@@ -144,16 +164,10 @@ def main():
144
164
  parser.add_argument("--digest", default="", help="文章摘要")
145
165
  parser.add_argument("--content-file", required=True, help="HTML内容文件路径")
146
166
  parser.add_argument("--cover-dir", required=True, help="封面和图片目录")
147
- parser.add_argument("--theme", default="abstract", help="图片主题")
167
+ parser.add_argument("--theme", default="books", help="图片主题")
148
168
  parser.add_argument(
149
169
  "--image-count", type=int, default=6, help="主题插图数量(默认6:5正文+1结尾)"
150
170
  )
151
- parser.add_argument(
152
- "--cover-seed",
153
- type=int,
154
- default=0,
155
- help="封面图候选序号(0/1/2,相同主题下可轮换)",
156
- )
157
171
  args = parser.parse_args()
158
172
 
159
173
  with open(args.content_file, "r", encoding="utf-8") as f:
@@ -167,7 +181,6 @@ def main():
167
181
  args.cover_dir,
168
182
  args.theme,
169
183
  args.image_count,
170
- args.cover_seed,
171
184
  )
172
185
 
173
186