Cross-platform content deconstruction + generation + knowledge graph. The bottom layer is a graph that grows over time; the top layer hosts multiple modes across multiple platforms.
中文版 / Chinese: see
~/.agents/skills/content-engine/
ModeStatusDescriptiondeconstruct✅ v1Reference link → 18-field deconstruction card; feeds the graph along the waygenerate✅ v2.2Deconstruction + graph + your brand → script / caption / cover / desc / tags / reference frames. v0.3.0: real video generation via Volcengine Ark Seedance 2.0 (sequential per-shot generation + ffmpeg auto-concat into final-video.mp4; partial-video.md tracks failed shots so you can re-run them). v0.2.1: built-in validator + auto-fallback v1.evaluate🔜 v3Finished content → 8-dimension weighted scoring
PlatformStatusCoverage / PlanXiaohongshu (XHS / RedNote)✅ v1Video posts + image postsDouyin🔜 v1.1Short-form video (planned via TikHub Douyin API)WeChat Channels (视频号)🔜 v1.2Short-form videoBilibili🔜 v2Short + long-form videoTikTok / Instagram🔜 ExploringInternational platforms
Current state: this document covers Xiaohongshu deconstruct (v1) + generate (v2.2 text+image+video). v0.3.0 ships real video via Seedance 2.0. Future platforms reuse the same architecture (
extract_{platform}.py/generate_{platform}.py+content_engine/{platform}/submodules); the graph is shared cross-platform.
Platform compatibility: This skill runs in OpenClaw (as a personal agent skill) and Claude Code. Scripts use Python 3.10+ stdlib (no external deps); the only system command needed is ffmpeg. File I/O assumes your agent has
Read/Writetools (in OpenClaw these map toapply_patch/Exec/Web browser).
text
content-engine-en/
├── SKILL.md ← what you're reading; the agent's entry point
├── graph/ ← knowledge graph (shared "memory / soul / context" across modes)
│ ├── index.md brand briefing, agent reads first
│ ├── brand/{brand-voice,brand-story}.md
│ ├── platforms/xiaohongshu.md XHS playbook (only platform in v1)
│ │ (v1.1+ will add douyin.md / wechat-channels.md / ...)
│ ├── audience/segments.md audience segmentation
│ └── engine/{hooks,style-tags,taboo}.md
├── references/{output-template,example-video,example-image}.md
└── scripts/
├── extract_xhs.py v1 deconstruct CLI: link → workspace
├── generate_xhs.py v2 generate CLI: link → script + images + copy
│ (v1.1+ will add extract_douyin.py / generate_douyin.py)
└── content_engine/ Python package (zero deps)
├── client.py TikhubClient (v1)
├── parsers.py NoteData / Comment parsing (v1)
├── linkresolve.py short link → note_id (shared)
├── video.py download + ffmpeg frame extraction (v1)
├── images.py image post downloader (v1)
├── llm.py Ofox LLM client (v2)
├── nano_banana.py Ofox image generation (v2, Nano Banana Pro)
├── lookup.py link → card lookup + freshness (v2)
├── prompts.py 5 text prompt templates (v2)
├── generate.py generate mode orchestration (v2)
├── preflight.py environment self-check (v1+v2)
└── models.py dataclass definitions
Two ironclad rules:
graph/engine/. The graph grows.RequiredFieldNotes✅Reference linkXHS short link / long link / 24-char hex note_id / share text — all acceptedTask IDDefaults to AIC-{YYMMDD}-{seq}Content goale.g., "drive in-store traffic" / "DM acquisition" — affects "Takeaways" field
Markdown file → docs/deconstructions/{id}-{slug}.md.
Full field definitions in references/output-template.md. Examples in references/example-video.md / example-image.md.
Use your agent's native tools directly (avoid bash globstar / realpath compat issues):
SKILL.md lives)Read or Grep tools to scan graph/**/*.md for # TODO: markersSimplest: a single Grep call:
text
Grep pattern: "^# TODO:" path: <skill_root>/graph/ output_mode: files_with_matches
Files matchedModeBehavior0Brand-awareStep 5's "Target audience" / "Takeaways" must be generated from graph content; field-fill stage must read the relevant graph nodes≥1ObjectiveSkip brand-aware fields; append to output: "⚠️ graph/ not yet populated — recommend filling {list of TODO files}"
Mode A vs B differences in the "Takeaways" field: see the dual-version comparison at the end of references/example-video.md.
Wikilink convention
[[brand/brand-voice]]between graph files: this is Obsidian-style, pointing tograph/brand/brand-voice.md(no.mdsuffix). When you see[[X]], Read the corresponding file to load context.
One command does it all: link resolution / metadata fetch / comments fetch / video download + frame extraction / image download for image posts.
⚠️ v1 supports Xiaohongshu only. Douyin / WeChat Channels / Bilibili etc. are on the roadmap (v1.1+) with corresponding
extract_douyin.py/extract_wechat_channels.pyscripts.
bash
python3 scripts/extract_xhs.py "<XHS link / note_id / share text>"
# Default workspace: {tempdir}/content-engine/{note_id}/
# Custom: --out /your/path
First-run environment check:
bash
python3 scripts/extract_xhs.py --check
(Checks Python version / ffmpeg / TIKHUB_API_TOKEN / network / workspace writable. See "Setup" section below.)
Workspace artifacts (default {tempdir}/content-engine/{note_id}/, cross-platform):
FileContentHow agent uses itnote.jsonParsed NoteData dataclass (all fields pre-extracted)Read directly; maps to Step 5 field tablecomments.jsonParsed Comment list (with is_pinned heuristic flag)You (agent) read raw text in Step 5c and classify semantically — better than regex{note_id}.mp4Original video file (CDN direct download)Used by Step 4 frame extractionframes/frame_NNN.pngExtracted frames (auto fps based on duration: short <10s → 1.0, mid → 0.5, long >60s → 0.25)Step 4 reads frame by frameimages/image_NNN.jpgAll images for image posts (numbered in order)Step 4 reads image by image
Error handling:
comments.json written as {"_error": "..."}; "comment keywords" field becomes "⚠️ Not retrieved"Common flags:
--no-video skip video download (metadata-only mode)--no-comments skip comments--fps 1.0 force frame rate (default auto-adapts to duration)Always Read first:
graph/platforms/xiaohongshu.md — sections "What to focus on when deconstructing" + "Platform viral formulas" + "Taboos"graph/engine/style-tags.md — full style dictionary (Step 5 style tag field uses this)graph/engine/hooks.md — full hook library (Step 5 emotion-hook field uses this)If in brand-aware mode, also Read graph/brand/brand-voice.md + graph/brand/brand-story.md + graph/audience/segments.md.
Read frames in order ( frame_001.png ...). Mental-note for each frame: shot type / subject / action / background / props / camera direction. Don't output N rows of stream-of-consciousness — accumulate material for aggregation in next step.
Aggregate into time segments for the "Reference content deconstruction" field. Core rule: ✅ Good (aggregated + dense)❌ Bad (stream of consciousness or empty)7-12s | Camera: locked → slow push | Shot: close-up → extreme close-up
Visual: emerald-green collar and placket of vest, jade buttons + white beaded geometric embroidery, paired with white jade pendant necklace as styling demo7s | close-up | collar
8s | close-up | collar
9s | close-up | button
...7-12s | Visual: very pretty clothing detail, exquisite craftsmanship Merge rules:
Voiceover/subtitle text:
note.json.descVoiceover logic analysis: write in layers, each layer with timestamp + one-line function:
images/image_NNN.jpg)FieldSourcegraph required readingPlatformLink source—Target audiencenote.json.desc + hashtags + comment behaviorBrand-aware mode: must cross-reference graph/audience/segments.md and explicitly mark which segment hitViral themenote.json.desc + title + deconstruction—Style tagsVisuals + copyMust cross-reference graph/engine/style-tags.md — mark "existing" if hit, "new" if not (and queue for Step 6 writeback)Scene tagsVisuals—Emotion hookOpening + hook linesMust cross-reference graph/engine/hooks.md patterns; explicitly mark which class hitComment keywordscomments.json (you classify yourself)See Step 5c — agent reads raw comments and classifies semantically; more accurate than regexVoiceover logic analysisCopy structure—Reference hashtagsnote.json.hashtags (parser already cleaned [话题])Parser pre-extracted; just join with #; don't grep descTakeawaysGlobal summaryStrong dependency: brand-aware mode writes "how we'd do the same theme"; objective mode writes general principles
See "Field definitions + Anti-Pattern" section in references/output-template.md. Core principles:
Why no regex: language has infinite variations ("how do I buy" / "what's the price" / "is it pricey" / "how much"), regex always misses; regex also can't handle semantics ("price isn't a problem" isn't an inquiry; "isn't this silk?" isn't an objection). You (agent) have full language understanding — do this directly, you're 100x better than regex at this.
Data source: comments.json already filtered by parser — is_pinned=True (merchant-pinned / anti-scam) is auto-flagged and skippable; the rest are real user comments.
Four classes (by "what the user is doing"):
ClassWhat to captureExamplesaskAsking about purchase path / price / address / hours / channels (pre-conversion info)"how do I buy" / "how much" / "where's the store" / "open hours" / "available online?"requestActive need (strong intent)"need WeChat" / "still in stock?" / "size out?" / "need contact"praiseResonance / specific likes"so beautiful" / "want it" / "elegant" / "love it" / "tempting"objectionCorrection / disagreement ( not neutral questions)"please don't call this X" / "this is A not B" / "shouldn't be this expensive"
Output format (mandatory evidence):
text
- {keyword label} ({N} raw comments: "text 1" "text 2" "text 3") — {one-line interpretation / conversion signal judgment}
Hard anti-fabrication rules:
[] or has _error → write "⚠️ Comment data not retrieved", don't infer from descGood vs Bad:
text
✅ Good (with evidence + interpretation):
- how-do-i-buy (5 raw comments: "how do I buy the green pants" "how to purchase, online?" "how do I buy this love the green") —
highest-frequency conversion signal
- how-much / pricing (2 raw comments: "how do you sell this" "what's the price for this set") —
another way of asking pricing
- objection-traditional-attire (1 raw comment: "this is Manchu attire, please don't call it 新中式" 👍 1) —
only 1 comment but liked, signals tag-usage edge case
❌ Bad (no evidence / fabricated):
- how-do-i-buy, how-much, need-link (just listing words, no raw text — forbidden)
❌ Bad (misclassifying questions as objections):
- objection (comment: "is this silk?") ← this is a question, not an objection
After deconstruction, proactively review and write back:
Strict writeback location rules:
TypeFileInsert locationFormatNew hookgraph/engine/hooks.mdEnd of ## Pending classification section### {emotion-class|pattern-name} H3 + bullets (pattern/适用/example/source)New style taggraph/engine/style-tags.mdEnd of ## Pending table`Platform observationgraph/platforms/xiaohongshu.mdTop of ## Observation log (newest first)### {YYYY-MM-DD} · {one-line topic} + bullets (source/observation/data/inference)
Writeback principles:
Every line must ✓; failing one means you don't proceed to Step 7:
text
□ All 18 Excel fields filled, no skips
□ All 5 metadata items (author/time/engagement/note_id/type) pulled live from API, not fabricated
□ Style / Scene / Emotion-hook are not confused (see output-template.md)
□ Reference body copy is desc original (with emojis + line breaks), not paraphrased or trimmed
□ Each comment keyword backed by raw text from comments.json; if comments.json has `_error`, write "⚠️ Not retrieved"
□ Voiceover logic analysis is written in layers (hook/setup/elevation/CTA), not a single paragraph
□ Style tags hitting graph dictionary are marked "existing"; new ones marked "new"
□ Emotion hook hitting existing graph pattern is explicitly noted; new patterns queued for Step 6 writeback
□ Reference hashtags pulled directly from note.json.hashtags (parser already cleaned [话题])
□ Step 6 writeback: explicitly state "N items" or "none"; each item has source/date
□ Objective mode: append "graph/ not populated" notice at the end
From title, generate filename / doc-name slug:
python
import re
slug = re.sub(r"[^\w一-龥\-_·]+", "-", title)[:30].strip("-") or "untitled"
# e.g., "Shenzhen 新中式|what does wearing 江南春色 feel like" → "Shenzhen-..."
Final naming: {id}-{slug} (e.g., AIC-260426-001-Shenzhen-deep-dive).
Preferred: Feishu (Lark) Docx (when running in OpenClaw with the Lark official plugin)
The OpenClaw Lark plugin gives the agent native tools to create cloud documents. In OpenClaw:
{id}-{slug}Fallback: local markdown (Claude Code / no Lark plugin / Lark tool failed)
bash
# Use the Write tool to write to:
docs/deconstructions/{id}-{slug}.md
Decision logic:
Note: This skill does NOT wrap Feishu API. The OpenClaw Lark plugin handles auth / upload / conversion; the agent only needs to call the plugin's tools. Claude Code users who want Feishu publishing must manually copy the markdown into a Feishu doc.
text
1. Subject: {title / author / duration / engagement} — one line
2. Strongest insight: {1 core hook or counter-intuitive finding} ({data evidence, e.g., "save-to-like ratio 70%"})
3. Published to: {Feishu URL or local absolute path}
4. Graph writeback: {N items; list top 3, abbreviate rest; if 0, explicitly state "none"}
Use the v1 deconstruction card as a competitor reference + your brand info → generate your own version of script / copy / reference frames / cover / tags.
User says something like:
RequiredFieldNotes✅XHS linkUser passes only the link; agent doesn't ask for filename✅--typevideo / image / script✅--count1-N (image count / video count)--product-imgsPath to product images (dir or single file) — text-only in v2.0; image-to-image in v2.1--product-uspFree-text USP / material / craft description--freshForce re-deconstruct v1 (bypass cache)
text
docs/deconstructions/AIC-260426-001-xxx-generated/ ← v1 card name + "-generated"
└── GEN-260427-001-image/ ← one GEN-N per generate run
├── script.md ← full script (image plan / video shots / shoot brief)
├── caption.txt ← (video type) on-screen captions
├── cover.png + cover.txt ← cover image (with overlay text) + text backup
├── frames/frame_NNN.png ← N reference images (Nano Banana, vertical 9:16)
├── desc.txt ← XHS post body
├── tags.txt ← hashtags (10-15)
├── seedance-prompt.md ← (video type) Seedance cinema-style prompt
├── shots/shot_NN.mp4 ← (video, v0.3.0) Per-shot real videos from Seedance
├── final-video.mp4 ← (video, v0.3.0) ffmpeg-concatenated final video
└── partial-video.md ← (video, v0.3.0) Per-shot status + failed-shot prompts
docs/deconstructions/ for note_idextract_xhs.py to fetch note.json + comments.json + frames, then writes a stub deconstruction card (text fields populated; visual fields marked ⚠️ AUTO-STUB for the agent to complete by reading frames/)script.mdN frames (per --count for image; 1 key frame for video) + 1 cover
Nano Banana three-path constraint prompt:
Hard rules baked into build_prompt:
--no-real-video)partial-video.md records which failed + their prompts for manual re-runffmpeg concat into final-video.mp4--no-real-video (prompt-only, skip API) / --async (submit only, return task_ids) / --no-confirm (skip 3s countdown)quality_report.md for the user to decide: desc length anomaly / too many emoji / multi-line covergraph/engine/taboo.md, layered on top of default extreme/marketing wordstext
1. Generated: based on {card} + {brand}, {type} ({count} items)
2. Outputs: script.md + cover + N frames + desc + tags + (seedance-prompt)
3. Workspace: {absolute path}
4. Time / calls: {seconds} / {LLM calls} + {image calls}
bash
# 8 reference images for an image post
python3 scripts/generate_xhs.py "<XHS link>" --type image --count 8 \
--product-usp "Premium knitwear: silk vest + embroidered shirt" \
--product-imgs ~/photos/spring-2026/
# 1 video (v0.3.0+: real video gen by default, ~$1, Ctrl+C to cancel)
python3 scripts/generate_xhs.py "<XHS link>" --type video --count 1
# 1 video, prompt only (script + cover + 1 key frame, no Seedance API call)
python3 scripts/generate_xhs.py "<XHS link>" --type video --count 1 --no-real-video
# Async: submit Seedance tasks and return immediately with task_ids
python3 scripts/generate_xhs.py "<XHS link>" --type video --count 1 --async
# Shoot brief only
python3 scripts/generate_xhs.py "<XHS link>" --type script --count 1
# Force re-deconstruct (bypass cache)
python3 scripts/generate_xhs.py "<XHS link>" --type image --count 8 --fresh
# Environment check (includes OFOX_API_KEY)
python3 scripts/generate_xhs.py --check
LimitationSolution directionPlanVideo not really generated (prompt only)Integrate Volcengine Ark Seedance 2.0 API✅ v0.3.0No hook variants (1 set per run)LLM multi-round with N hook directionsv0.4.xWeak character consistency (different faces)IP-Adapter / InstantIDv0.5.xNo automatic QAvalidator.py with hard+soft error detection✅ v0.2.1Fallback v1 deconstruct is manualAuto-trigger built in✅ v0.2.1Stub card visual fields filled by agent manuallyVision LLM auto-completionv0.4.0
--count 1 first to verify before scaling upDependencyPurposeInstallPython ≥ 3.10Run all scriptsmacOS: brew install python@3.12
Linux: apt install python3.12 or pyenv
Windows: python.orgffmpegVideo frame extraction (optional for image-only)macOS: brew install ffmpeg
Linux: apt install ffmpeg (or dnf / pacman)
Windows: choco install ffmpeg
No pip dependencies — scripts use Python stdlib only.
v1 deconstruct needs TIKHUB_API_TOKEN (required for deconstruct)
v2 generate needs OFOX_API_KEY (required for generate; covers LLM + Nano Banana image gen)
v0.3.0+ real video needs ARK_API_KEY (required when video type runs in default mode; --no-real-video bypasses)
bash
# v1 deconstruct: TikHub
mkdir -p ~/.config/content-engine
echo 'TIKHUB_API_TOKEN=your_tikhub_token' >> ~/.config/content-engine/.env
# v2 generate: Ofox (LLM text + Nano Banana images)
echo 'OFOX_API_KEY=ofox-your_key' >> ~/.config/content-engine/.env
# v0.3.0+ video generation: Volcengine Ark (Seedance 2.0)
echo 'ARK_API_KEY=your_ark_key' >> ~/.config/content-engine/.env
TokenSign upUseRequired?TIKHUB_API_TOKENtikhub.ioXHS API (raw deconstruct data)v1 deconstructOFOX_API_KEYofox.aiLLM + Nano Banana imagesv2 generateARK_API_KEYVolcengine Ark ConsoleSeedance 2.0 video generationrequired for v0.3.0+ real video; --no-real-video bypassesOPENROUTER_API_KEYopenrouter.ai(optional) alternate LLM provideroptional
⚠️ The ARK API Key is not the same as a Volcengine IAM AK/SK (both are UUID-shaped but use different auth). Create it under "API Key Management" in the Ark console, then enable
Doubao-Seedance-2.0-fastunder "Activation → Vision Models" (default 5M tokens free).Switch model:
export ARK_VIDEO_MODEL=doubao-seedance-1-5-pro-251215(or any other Ark model id).
Token lookup order (first found wins):
TIKHUB_API_TOKEN / OFOX_API_KEY / ARK_API_KEY / OPENROUTER_API_KEY)$CWD/.env~/.config/content-engine/.env (XDG standard).envbash
python3 scripts/extract_xhs.py --check
Reports each check ✅/❌/⚠️ with fix instructions.
api.tikhub.io requires a proxy from inside Chinaapi.tikhub.dev (no proxy needed) — set TIKHUB_BASE_URL=https://api.tikhub.dev in .envThis skill does not bundle Feishu API code. To enable auto-publishing of deconstruction cards to Feishu Docx, install the OpenClaw Lark official plugin:
bash
npx -y @larksuite/openclaw-lark install
Details: OpenClaw Lark official plugin docs
Once installed:
Claude Code or other environments: deconstruction cards save to local docs/deconstructions/; copy to Feishu manually if needed.
analyze-xhs skill: account-level analysis (not single-post)~/.agents/skills/content-engine/