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 +10 -17
- package/package.json +1 -1
- package/scripts/__pycache__/book_cover.cpython-314.pyc +0 -0
- package/scripts/book_cover.py +61 -63
- package/scripts/publish.py +21 -24
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
|
-
|
|
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
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
`
|
|
225
|
+
**支持的主题**:`abstract` / `books` / `nature` / `technology` / `business`
|
|
233
226
|
|
|
234
227
|
### 第三步:上传图片到微信
|
|
235
228
|
|
package/package.json
CHANGED
|
Binary file
|
package/scripts/book_cover.py
CHANGED
|
@@ -124,22 +124,65 @@ def fetch_book_cover(book_title, author="", isbn=""):
|
|
|
124
124
|
return cover_url
|
|
125
125
|
|
|
126
126
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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下载并处理为
|
|
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
|
-
|
|
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
|
-
|
|
168
|
-
|
|
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
|
-
"""
|
|
191
|
-
|
|
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__":
|
package/scripts/publish.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
53
|
-
|
|
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
|
|