publish-md-to-wechat
Publishes Markdown articles to WeChat Official Account drafts with professional visual styling, automatic image handling, and cover generation.
Operating Modes (strict boundary)
Mode A — Article Publish (公众号图文)
Use when user wants Markdown -> WeChat draft publishing.
- Inputs:
--md, style, optional thumb/title - Output: WeChat draft media id
- Command family:
scripts/wechat_publisher.py
Mode B — Video Render (视频号)
Use when user wants vertical MP4 generation.
- Inputs: source article plus duration, tone, audience, visual style, and optional Edge TTS voice
- Output: local
.mp4 - Command family:
scripts/video_publisher.py - Important: The agent plans the video and creates the project files; the script renders, narrates, subtitles, composes, and captures validation frames.
Mode C — Translation & Repost (全文翻译/转载)
Use when user wants to translate a foreign-language article and publish it to WeChat.
- Trigger: "全文翻译"、"转载"、"translate"、"repost"、or source article is in English/other language.
- Inputs: source Markdown file (English or other language)
- Output: WeChat draft media id (same as Mode A)
- Workflow: Agent translates natively (no Python script) → saves to
/tmp/translated_article.md→ proceeds as Mode A. - Important: Translation is done by the agent itself, not by any script. Do not invoke an LLM via Python for translation.
Do not mix Mode A and Mode B steps in one flow unless user explicitly asks for both deliverables. Mode C always ends with Mode A publishing — it is a pre-processing step, not a separate pipeline.
Phase 1: Environment & Credentials
Check dependencies:
ls .venv/bin/python 2>/dev/null || echo "MISSING"
If .venv is missing, run ./install.sh.
Credentials: Loaded automatically from .env in the current directory, or
~/.config/publish-md-to-wechat/.env as global fallback. Only ask the user for credentials
if the script explicitly fails with an auth error.
Phase 1B: Mode C — Translation & Repost
Do not summarize or adapt — translate the full article faithfully.
- Read the source Markdown file completely.
- Translate the entire content to Chinese, preserving:
- All Markdown structure (headings, lists, code blocks, blockquotes, images)
- Original section order and hierarchy
- Technical terms in English where standard (e.g. agent, token, context window, bash)
- Code blocks verbatim — do not translate code
- Author attribution in frontmatter:
> 本文译自:[原文标题](url),作者:[Author]([Publication])
- Write YAML frontmatter with
title,author,descriptionin Chinese. - Save to
/tmp/translated_article.md. - Proceed to Phase 2 with the translated file as
--md.
Do not ask for confirmation before translating — if the user said "全文翻译/转载", just do it.
Phase 1C: Frontmatter Adaptation (Vault Articles)
Before publishing any Vault article, verify the frontmatter contains the fields WeChat publisher needs to render the article header correctly.
The publisher renders a metadata block at the top of every article using these specific keys:
| WeChat key | Renders as | Common Vault alias | Action |
|---|---|---|---|
author |
作者 | same | ✅ usually present |
published |
发布日期 | date |
⚠️ add published: if only date: exists |
tags |
标签列表 | — | ⚠️ add if missing |
description |
摘要 | excerpt |
⚠️ add description: if only excerpt: exists |
source |
来源 | — | ⚠️ add source: AI-DIVE · ai.air7.fun for Vault articles |
Script fallbacks (built-in): The publisher script automatically falls back:
published→dateifpublishedis absentdescription→excerptifdescriptionis absent
So these alias pairs do not need to be manually patched before publishing. The remaining three
(author, tags, source) have no automatic alias — add them to frontmatter if missing.
Minimal complete frontmatter for a Vault → WeChat article:
---
title: 文章标题
slug: type-YYYY-MM-DD-topic # ai-dive API
date: YYYY-MM-DD # ai-dive API
published: YYYY-MM-DD # WeChat header display
type: analysis # ai-dive API
author: Rafa
tags: [标签1,标签2, 标签3] # WeChat header display
excerpt: 摘要文字 # ai-dive API
description: 摘要文字 # WeChat header display (same content as excerpt)
source: AI-DIVE · ai.air7.fun # WeChat header display
og_image: https://... # ai-dive cover (fill after upload)
cover: assets/topic-cover.png # local preview
---
Note: date and published carry the same value; excerpt and description carry the same
content — the two systems use different keys for the same concept.
Phase 2: Read the Article & Select a Style
Read the Markdown file to understand the content's tone. Then pick a style.
List all styles:
.venv/bin/python3 scripts/styles.py --list
Style selection guide:
| Category | Styles | When to use |
|---|---|---|
| ⭐ Core | swiss, editorial, ink |
Default choice. Covers 90% of content types. |
| 📦 Extend | notebook, geometry, botanical, terminal, bold, cyber, voltage |
Specific scenarios only. |
| 🎨 Custom | custom-xxx |
User-created from WeChat article analysis. |
Core trio quick guide:
swiss— Technical articles, tutorials, reports, official announcementseditorial— Opinion pieces, analysis, blog posts, personal brandsink— Humanities, culture, deep dives, literary content
Propose the style with a one-line reason. Skip if the user already specified.
Phase 3: Validate First (Recommended for complex articles)
.venv/bin/python3 scripts/wechat_publisher.py \
--md [PATH] --style [STYLE] --dry-run --out-html /tmp/preview.html
open /tmp/preview.html
Phase 4: Publish
.venv/bin/python3 scripts/wechat_publisher.py \
--md [PATH] --style [STYLE]
Key flags:
--thumb [PATH]— Custom cover (auto-generates if omitted)--title "[TITLE]"— Override title (auto-detects from frontmatter/H1)--no-verify-ssl— For SSL cert issues-v— Verbose logging
YAML Frontmatter: Title, author, description auto-extracted:
---
title: Article Title
author: Author Name
description: Short summary for WeChat article list.
---
Phase 4B: Generate WeChat Video (视频号)
By default, ALWAYS generate video WITH TTS voice unless explicitly told not to. Important: Before generating the video, you MUST ask the user for the following preferences if they haven't provided them explicitly in their request:
- Target Duration (e.g., 30s, 60s, 90s)
- Narration Tone (e.g., 专业克制, 轻松幽默, 热情洋溢)
- Target Audience (e.g., AI开发者, 产品经理, 大众)
- Visual Style (e.g., swiss, ink, minimal)
- TTS Voice (optional; default
zh-CN-YunxiNeural)
Do not guess or assume these 5 parameters. If the user simply says "convert to video", you MUST stop and ask them to specify duration, tone, audience, and style first.
Step 1: Agent Plans Scenes (DO NOT use Python scripts for LLM calls)
Read the Markdown file. Act as an expert presentation editor. Natively create a video project directory containing scenes.txt and gen_scenes.py, based on the requested duration, tone, audience, and visual style.
Create the project first; do not start from a blank Python file:
.venv/bin/python3 scripts/video_publisher.py --init-project /tmp/video-project
Then edit /tmp/video-project/scenes.txt and /tmp/video-project/gen_scenes.py. The bundled generator is the visual baseline: retain its 1080×1920 canvas, brand line, page number, bottom bar, contrast, and common CSS; adapt the copy and layouts for the story.
CRITICAL: Narration Quality Rules (avoid filler/repetition)
- Audience/tone are global constraints, not per-scene slogans. Do not repeatedly say phrases like
面向AI开发者/关键在于in every scene. - Each scene narration must add new information (fact, example, contrast, action step), not template padding.
- Ban empty framing lines:
我们来看看,关键在于,总的来说unless they introduce concrete content right after. - Keep narration concise and spoken: typically 1-3 short sentences per scene; avoid paragraph-long monologues.
- Avoid repeating the same sentence pattern across scenes.
- Prefer concrete wording over abstract buzzwords.
Narration self-check before writing files:
- Read all scene narrations end-to-end.
- If any phrase stem repeats in 3+ scenes (e.g.,
面向…,关键在…), rewrite. - Ensure each scene has a unique takeaway in one line.
- Remove all non-informational filler.
CRITICAL: HTML Scene Formatting for Vertical Video (9:16)
scenes.txtcontains one ordered scene per line:S1|spoken narration|optional written subtitle.gen_scenes.pymust generatescenes/S1.html,scenes/S2.html, and so on at 1080×1920.- Use a shared CSS system in the generator: stable brand line, page number, bottom information bar, high-contrast text, and a restrained accent color.
- The spoken and subtitle text can differ for URLs and English terms. For example, speak
air7 点 funbut subtitleair7.fun. - Keep each scene to one idea. The script derives timing from the generated audio, adds a short breathing interval, and burns subtitles into the video.
Step 2: Render and Compose (execution-only) Use the script to generate HTML, capture PNG scenes, synthesize narration, create subtitles, compose the MP4, and capture start/middle/end QA frames.
# 导出 MP4
.venv/bin/python3 scripts/video_publisher.py \
--project-dir /tmp/video-project \
--duration [SECONDS] \
--voice zh-CN-YunxiNeural \
--rate +6% \
--out [OUTPUT.mp4]
依赖:ffmpeg、Python Playwright Chromium 和 edge-tts。
Step 3: Mandatory visual QA
Open build/check_start.jpg, build/check_middle.jpg, and build/check_end.jpg. Check subtitle line breaks, readable contrast, overflowing copy, spelling, and narration-to-scene alignment. If any frame fails, modify only the affected scene in scenes.txt / gen_scenes.py, then rerun the renderer. Do not report a video as complete without passing this check.
Phase 5: Success Output
## 🚀 发布成功!
| 字段 | 值 |
|------|-----|
| **标题** | [Title] |
| **风格** | `[style]` — [one-line description] |
| **Media ID** | `[MEDIA_ID]` |
**下一步:** mp.weixin.qq.com → 草稿箱 → 预览 → 发布
Custom Styles
Style replication uses Playwright getComputedStyle() (not inline CSS parsing) for accurate colors,
and automatically detects structural patterns:
- Heading style:
bg-block(colored section bg) /left-border/underline/plain - Blockquote style:
left-border/full-box/plain
The renderer applies these structural hints when publishing with a custom style.
# Create from WeChat article (give it a meaningful name)
.venv/bin/python3 scripts/styles.py --url https://mp.weixin.qq.com/s/xxx --name custom-myname --no-verify-ssl
# List all styles (core / extend / custom)
.venv/bin/python3 scripts/styles.py --list
# Rename
.venv/bin/python3 scripts/styles.py --rename custom-old custom-new
# Use
.venv/bin/python3 scripts/wechat_publisher.py --md article.md --style custom-xxx
Troubleshooting
| Error | Fix |
|---|---|
| 40164 IP whitelist | Add IP at mp.weixin.qq.com → 设置与开发 → IP白名单 |
| 40125 / 40013 Invalid credentials | Check .env |
| 45009 Rate limit | Auto-retries; wait if persists |
| 45166 invalid content (anchor links) | Article contains href="#anchor" links (e.g. table of contents). These are auto-stripped by the post-processor since v0.7.0. If you see this on older versions, upgrade. |
| SSL errors | Add --no-verify-ssl |
| Image not found | Check filename (case-sensitive), ensure image is in article directory |
| Cover failed | Falls back to assets/default_thumb.png; or use --thumb |
| Missing dependencies | Run ./install.sh |
Known WeChat Rendering Constraints
These are platform-level limitations in WeChat's HTML renderer, handled automatically by the post-processor since v0.7.0:
| Constraint | Behaviour | Automatic Fix |
|---|---|---|
data-src required on images |
WeChat lazy-loads images using data-src. Images with only src render as blank. |
Post-processor adds data-src to every <img> after URL upload. |
<ol>/<li> whitespace nodes |
Whitespace text nodes between <li> tags are rendered as extra blank numbered items. |
Post-processor rebuilds the entire .footnotes section as flat <section> elements with inline numbering. |
Anchor links (href="#") blocked |
Draft API returns 45166 if any <a href="#..."> is present. |
Post-processor unwraps all #-prefixed <a> tags to plain text. |
| Footnote backlinks | mistune inserts a ↩ return link at the end of each footnote which is meaningless in WeChat. |
Post-processor removes all <a class="footnote"> tags and strips residual ↩ characters. |