wechat-media-writer 2.2.4 → 2.2.5

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
@@ -208,28 +208,21 @@ from image_downloader import download_theme_images
208
208
  urls = download_theme_images("books", "/tmp/images", count=6)
209
209
  ```
210
210
 
211
- ### 第二步:制作公众号封面图
211
+ ### 第二步:下载公众号封面图(精确 900x500)
212
212
 
213
- 公众号封面(`cover_900x500.jpg`)用主题贴图中的一张裁剪为 900x500 比例:
213
+ **公众号封面必须是 900x500 精确尺寸的精美图片**,与正文主题风格统一。
214
+ 不再从主题贴图裁剪(裁剪可能损失画面关键内容)——直接从 Pexels CDN 请求 900x500 精确裁切。
215
+
216
+ 每个主题预存 3 张高质量 Pexels 候选图(见 `scripts/book_cover.py` 的 `THEME_COVER_CANDIDATES`),
217
+ 通过 `?w=900&h=500&fit=crop` 参数让 Pexels 服务端精确裁切为 900x500:
214
218
 
215
219
  ```python
216
- from PIL import Image
217
- img = Image.open("img_1.jpg")
218
- w, h = img.size
219
- target_ratio = 900 / 500
220
- if w / h > target_ratio:
221
- new_w = int(h * target_ratio)
222
- left = (w - new_w) // 2
223
- img = img.crop((left, 0, left + new_w, h))
224
- else:
225
- new_h = int(w / target_ratio)
226
- top = (h - new_h) // 2
227
- img = img.crop((0, top, w, top + new_h))
228
- img = img.resize((900, 500), Image.LANCZOS)
229
- img.save("cover_900x500.jpg", "JPEG", quality=85)
220
+ from book_cover import download_cover_900x500
221
+ # 下载一张 books 主题的 900x500 封面(seed=0/1/2 轮换候选图)
222
+ download_cover_900x500("books", "/tmp/wx", seed=0)
230
223
  ```
231
224
 
232
- `scripts/book_cover.py` `download_and_process_cover()` 函数会从主题贴图(`img_1.jpg`)裁剪生成 900x500 封面。
225
+ **支持的主题**:`abstract` / `books` / `nature` / `technology` / `business`
233
226
 
234
227
  ### 第三步:上传图片到微信
235
228
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wechat-media-writer",
3
- "version": "2.2.4",
3
+ "version": "2.2.5",
4
4
  "description": "微信公众号书评、影评文章自动生成与发布 - Skill",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -124,22 +124,65 @@ def fetch_book_cover(book_title, author="", isbn=""):
124
124
  return cover_url
125
125
 
126
126
 
127
- def fetch_theme_cover(theme="abstract"):
128
- """从 Pexels CDN 获取与主题贴图一致的主题封面(推荐使用)"""
129
- theme_urls = {
130
- "abstract": "https://images.pexels.com/photos/2693208/pexels-photo-2693208.jpeg?auto=compress&cs=tinysrgb&w=1080",
131
- "books": "https://images.pexels.com/photos/159711/books-bookstore-book-reading-159711.jpeg?auto=compress&cs=tinysrgb&w=1080",
132
- "nature": "https://images.pexels.com/photos/2387873/pexels-photo-2387873.jpeg?auto=compress&cs=tinysrgb&w=1080",
133
- "technology": "https://images.pexels.com/photos/3568520/pexels-photo-3568520.jpeg?auto=compress&cs=tinysrgb&w=1080",
134
- "business": "https://images.pexels.com/photos/3184292/pexels-photo-3184292.jpeg?auto=compress&cs=tinysrgb&w=1080",
135
- }
136
- url = theme_urls.get(theme, theme_urls["abstract"])
137
- print(f"✓ 主题封面URL ({theme}): {url}")
127
+ # 每个主题对应 3 张精选的 900x500 Pexels 封面图(fit=crop 自动精确裁切)
128
+ # 选用宽高比接近 1.8 的高质量原图,避免裁切损失重要内容
129
+ THEME_COVER_CANDIDATES = {
130
+ "abstract": [1108572, 1762851, 3109808],
131
+ "books": [256450, 590493, 762687],
132
+ "nature": [2387873, 2387874, 2387876],
133
+ "technology": [3568520, 1181244, 546819],
134
+ "business": [3184292, 210607, 3184296],
135
+ }
136
+
137
+
138
+ def _build_cover_url(pid, w=900, h=500):
139
+ """构造 Pexels CDN URL,含精确 900x500 fit=crop 裁切参数"""
140
+ return f"https://images.pexels.com/photos/{pid}/pexels-photo-{pid}.jpeg?auto=compress&cs=tinysrgb&w={w}&h={h}&fit=crop"
141
+
142
+
143
+ def fetch_theme_cover(theme="abstract", seed=0):
144
+ """从 Pexels CDN 获取一张与主题对应的 900x500 精确封面 URL(不裁剪原图)"""
145
+ candidates = THEME_COVER_CANDIDATES.get(theme, THEME_COVER_CANDIDATES["abstract"])
146
+ pid = candidates[seed % len(candidates)]
147
+ url = _build_cover_url(pid)
148
+ print(f"✓ 主题封面URL ({theme}, pid={pid}): {url}")
138
149
  return url
139
150
 
140
151
 
152
+ def download_cover_900x500(theme="abstract", output_dir="/tmp", seed=0):
153
+ """下载一张 900x500 的主题封面图(Pexels 服务端精确裁剪,无需本地处理)
154
+
155
+ 返回: cover_900x500.jpg 绝对路径
156
+ """
157
+ ctx = ssl.create_default_context()
158
+ ctx.check_hostname = False
159
+ ctx.verify_mode = ssl.CERT_NONE
160
+
161
+ os.makedirs(output_dir, exist_ok=True)
162
+ cover_900_path = os.path.join(output_dir, "cover_900x500.jpg")
163
+
164
+ candidates = THEME_COVER_CANDIDATES.get(theme, THEME_COVER_CANDIDATES["abstract"])
165
+ # 顺序尝试每个候选 PID,跳过 404
166
+ for i, pid in enumerate(candidates):
167
+ idx = (seed + i) % len(candidates)
168
+ pid_try = candidates[idx]
169
+ url = _build_cover_url(pid_try)
170
+ try:
171
+ req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
172
+ data = urllib.request.urlopen(req, context=ctx, timeout=15).read()
173
+ with open(cover_900_path, "wb") as f:
174
+ f.write(data)
175
+ print(f"✓ 公众号封面已下载 ({theme}, pid={pid_try}): {cover_900_path}")
176
+ return cover_900_path
177
+ except Exception as e:
178
+ print(f" 候选 pid={pid_try} 失败: {e}")
179
+ continue
180
+
181
+ raise RuntimeError(f"主题 {theme} 的所有候选封面均下载失败")
182
+
183
+
141
184
  def download_and_process_cover(cover_url, output_dir):
142
- """从主题贴图URL下载并处理为 900x500 公众号封面 + 600px 文章封面"""
185
+ """从主题贴图URL下载并处理为 600px 文章封面(兼容旧调用)"""
143
186
  from PIL import Image
144
187
 
145
188
  ctx = ssl.create_default_context()
@@ -154,66 +197,21 @@ def download_and_process_cover(cover_url, output_dir):
154
197
  with open(original_path, "wb") as f:
155
198
  f.write(data)
156
199
 
157
- # 文章内配图(600px 宽)
158
200
  img = Image.open(original_path)
159
201
  w, h = img.size
160
202
  if w > 600:
161
203
  ratio = 600 / w
162
- new_w = 600
163
- new_h = int(h * ratio)
164
- img = img.resize((new_w, new_h), Image.LANCZOS)
204
+ img = img.resize((600, int(h * ratio)), Image.LANCZOS)
165
205
  img.save(os.path.join(output_dir, "theme_cover_600.jpg"), "JPEG", quality=90)
166
206
 
167
- # 公众号封面(900x500)
168
- img2 = Image.open(original_path)
169
- w, h = img2.size
170
- target_ratio = 900 / 500
171
- if w / h > target_ratio:
172
- new_w = int(h * target_ratio)
173
- left = (w - new_w) // 2
174
- img2 = img2.crop((left, 0, left + new_w, h))
175
- else:
176
- new_h = int(w / target_ratio)
177
- top = (h - new_h) // 2
178
- img2 = img2.crop((0, top, w, top + new_h))
179
- img2 = img2.resize((900, 500), Image.LANCZOS)
180
- cover_900_path = os.path.join(output_dir, "cover_900x500.jpg")
181
- img2.save(cover_900_path, "JPEG", quality=90)
182
-
183
- print(f"✓ 主题封面已保存: theme_cover_600.jpg")
184
- print(f"✓ 公众号封面已保存: {cover_900_path}")
185
-
186
- return os.path.join(output_dir, "theme_cover_600.jpg"), cover_900_path
207
+ print(f"✓ 主题封面(600px)已保存: theme_cover_600.jpg")
208
+ return os.path.join(output_dir, "theme_cover_600.jpg"), None
187
209
 
188
210
 
189
211
  def crop_cover_from_local(source_path, output_dir):
190
- """从本地主题贴图裁剪为 900x500 公众号封面(用于已有主题图但缺封面图的情况)"""
191
- from PIL import Image
192
-
193
- if not os.path.exists(source_path):
194
- print(f"错误: 源图不存在 {source_path}", file=sys.stderr)
195
- return None
196
-
197
- os.makedirs(output_dir, exist_ok=True)
198
- img = Image.open(source_path)
199
- w, h = img.size
200
- target_ratio = 900 / 500
201
- if w / h > target_ratio:
202
- new_w = int(h * target_ratio)
203
- left = (w - new_w) // 2
204
- img = img.crop((left, 0, left + new_w, h))
205
- else:
206
- new_h = int(w / target_ratio)
207
- top = (h - new_h) // 2
208
- img = img.crop((0, top, w, top + new_h))
209
- img = img.resize((900, 500), Image.LANCZOS)
210
-
211
- cover_900_path = os.path.join(output_dir, "cover_900x500.jpg")
212
- img.save(cover_900_path, "JPEG", quality=90)
213
- print(
214
- f"✓ 公众号封面已生成(基于 {os.path.basename(source_path)}): {cover_900_path}"
215
- )
216
- return cover_900_path
212
+ """兼容旧 API:建议改用 download_cover_900x500()"""
213
+ print("⚠️ crop_cover_from_local 已弃用,请改用 download_cover_900x500(theme=...)")
214
+ return None
217
215
 
218
216
 
219
217
  if __name__ == "__main__":
@@ -12,7 +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 crop_cover_from_local, fetch_theme_cover
15
+ from book_cover import download_cover_900x500
16
16
  from image_downloader import download_theme_images
17
17
  from wechat_api import (
18
18
  load_config,
@@ -42,30 +42,19 @@ def ensure_theme_images(cover_dir, theme="abstract", count=6):
42
42
  download_theme_images(theme, cover_dir, count)
43
43
 
44
44
 
45
- def ensure_cover_image(cover_dir, theme="abstract"):
46
- """确保公众号封面(900x500)存在,缺失则从主题图裁剪"""
45
+ def ensure_cover_image(cover_dir, theme="abstract", seed=0):
46
+ """确保公众号封面(900x500)存在,缺失则下载一张对应主题的精确尺寸封面
47
+
48
+ 注意:不再从主题插图裁剪(裁剪可能损失画面重要内容),
49
+ 而是从 Pexels 直接请求 900x500 精确尺寸的对应主题图片。
50
+ """
47
51
  cover_900_path = os.path.join(cover_dir, "cover_900x500.jpg")
48
52
  if os.path.exists(cover_900_path):
49
- print("✓ 公众号封面已存在,跳过裁剪")
53
+ print("✓ 公众号封面已存在,跳过下载")
50
54
  return
51
55
 
52
- # 优先用本地第一张主题图裁剪;否则从主题URL下载
53
- candidates = sorted(
54
- [
55
- f
56
- for f in os.listdir(cover_dir)
57
- if f.startswith("img_") and f.endswith((".jpg", ".jpeg", ".png"))
58
- ]
59
- )
60
- if candidates:
61
- crop_cover_from_local(os.path.join(cover_dir, candidates[0]), cover_dir)
62
- else:
63
- # 极少见:连主题图都没有的情况
64
- print("主题图与封面均缺失,从网络拉取主题封面...")
65
- from book_cover import download_and_process_cover
66
-
67
- url = fetch_theme_cover(theme)
68
- download_and_process_cover(url, cover_dir)
56
+ print(f"封面图缺失,下载一张 {theme} 主题的 900x500 封面...")
57
+ download_cover_900x500(theme, cover_dir, seed=seed)
69
58
 
70
59
 
71
60
  def publish_article(
@@ -76,8 +65,9 @@ def publish_article(
76
65
  cover_dir,
77
66
  theme="abstract",
78
67
  image_count=6,
68
+ cover_seed=0,
79
69
  ):
80
- """完整发布流程:准备主题贴图 -> 裁剪公众号封面 -> 上传图片 -> 创建草稿"""
70
+ """完整发布流程:准备主题贴图 -> 下载公众号封面 -> 上传图片 -> 创建草稿"""
81
71
  print("=" * 50)
82
72
  print("微信公众号 - 书评发布")
83
73
  print("=" * 50)
@@ -88,9 +78,9 @@ def publish_article(
88
78
  print("\n[1/4] 准备主题贴图...")
89
79
  ensure_theme_images(cover_dir, theme, image_count)
90
80
 
91
- # 2. 准备公众号封面(900x500
81
+ # 2. 准备公众号封面(900x500 精确尺寸)
92
82
  print("\n[2/4] 准备公众号封面...")
93
- ensure_cover_image(cover_dir, theme)
83
+ ensure_cover_image(cover_dir, theme, seed=cover_seed)
94
84
 
95
85
  # 3. 加载配置和获取 token
96
86
  config = load_config()
@@ -158,6 +148,12 @@ def main():
158
148
  parser.add_argument(
159
149
  "--image-count", type=int, default=6, help="主题插图数量(默认6:5正文+1结尾)"
160
150
  )
151
+ parser.add_argument(
152
+ "--cover-seed",
153
+ type=int,
154
+ default=0,
155
+ help="封面图候选序号(0/1/2,相同主题下可轮换)",
156
+ )
161
157
  args = parser.parse_args()
162
158
 
163
159
  with open(args.content_file, "r", encoding="utf-8") as f:
@@ -171,6 +167,7 @@ def main():
171
167
  args.cover_dir,
172
168
  args.theme,
173
169
  args.image_count,
170
+ args.cover_seed,
174
171
  )
175
172
 
176
173