Install
openclaw skills install @edison7009/tierlist-video-makerTurn a published TierVibe tier list into a narrated video. Fetches tier list data and card images from a TierVibe URL, captures a HIGH-RESOLUTION board image from the public page (Playwright, no server call — the whole-image export is a user-side action, automated), uses AI vision to identify each c
openclaw skills install @edison7009/tierlist-video-makerTurn any published TierVibe tier list into a narrated video with a scrolling high-resolution background.
Video composition preserves PNG/WebP alpha for card overlays. Transparent card images are resized with alpha-aware premultiplication and composited on a clean solid-black backing with no outline/border, so cutout/standee images stay readable without black halos, busy background bleed-through, or a semi-transparent glass-panel look.
This skill works on published TierVibe posts only. The video needs a board image and card images, which exist only once a post is published — a draft / a post you're still editing in the editor is not publicly readable and has no board image, so the scripts cannot fetch it.
If the user gives you a post that isn't published yet: stop and tell them —
"先把它在 TierVibe 上发布,再用这个技能做成视频" / "publish it on TierVibe
first, then make the video." Do not try to make a video from an editor URL or a
draft. fetch_tierlist.py checks the status field and will fail fast with this
same message if the post isn't published.
First check which kind of board this is — vision may not be needed at all.
After Step 2, look at each card's image_url and detail in manifest.json:
text:... → a TEXT card. The label is right there in the data (TierVibe
encodes it as text:<urlencoded label>#<fg>#<bg>); fetch_tierlist.py has
already rendered the swatch and filled label, tagging it
label_source: "text_card_data". Skip the vision work in Steps 4 and 5 —
read the labels straight from manifest.json. A model with no image
recognition can complete a text board end to end.
an http(s):// URL means an IMAGE card. Whether vision is needed depends on that card's detail:
detail - vision-free. The author's commentary names or clearly references the item, so you can DERIVE a short card label from the detail text (e.g. its first markdown heading, or the subject of the first sentence) without looking at the image. Write that label into the card's label field with label_source: "derived_from_detail", then skip the vision work in Steps 4 and 5 and go straight to Step 6. A model with no image recognition can complete these cards end to end, just like text cards.detail - vision IS needed. The API returns no text label for image cards, so the only way to know what the card depicts is to look at it. Everything below applies to these cards only.If the board mixes the two, handle each card by its own case: derive labels from detail where present, and use vision only for the no-detail image cards.
For no-detail image cards this skill needs AI vision: the API returns no text labels, so card names are baked into the images and you (the model running this skill) must look at each such card image and identify what it depicts before writing narration.
If you do NOT support image recognition (you can't view/identify images) AND there are no-detail image cards you can't otherwise name, stop at Step 4 and tell the user plainly - do NOT fabricate card names or hallucinate narration. Say, in the user's language:
"I can't make this video - some card images have no
detailand I can't recognize images. Either: (1) use a multimodal model that supports image recognition (e.g. GPT-4o, Claude with vision, Gemini), (2) add adetail(讲解) explanation to those cards in the TierVibe post and re-run, or (3) tell me what each of those cards is and I'll take it from there."
Image recognition is the model's own capability, not something this skill can work around - same principle as image search in the TierList-Maker skill. Be honest; do not pretend. But remember: a card with detail never needs vision - derive its label from the text.
The TierVibe server only stores a 600px thumbnail of the board, not a
high-resolution one. For a sharp 1080p background, this skill reproduces the
user-side "download whole image" action in a script: a headless Chromium opens
the public read page and runs the same html-to-image library the in-page
button uses. No TierVibe server call is made for the board image — the export
is a client-side action, just automated. See references/tiervibe-api.md.
python <skill_dir>/scripts/capture_board.py "<URL_OR_SLUG>" -o <work_dir> --pixel-ratio 2
Runs headless Chromium on the public https://tiervibe.com/t/<slug> page,
captures [data-testid="tier-grid"] to board_hd.png (~2560px wide). This
is captured FIRST because it is the visual source of truth for tier
assignment and card order — recognizing cards against this single image (with
tier labels and neighbors as context) is far more accurate than recognizing
each card image in isolation (user-reported bug: isolated recognition
misidentifies cards).
If this fails ("tier-grid not found"), the TierVibe deploy has not shipped the
data-testidattribute yet. Fall back to the 600px thumb from Step 2 — the video still works, the background is just softer — and note it to the user. Board-first recognition (Step 4) then falls back to per-card-only.
python <skill_dir>/scripts/fetch_tierlist.py "<URL_OR_SLUG>" -o <work_dir>
Downloads all card images + the 600px server thumb (fallback background), writes
manifest.json. Verify: total_cards > 0 and images/ is populated.
Only when Playwright cannot be installed at all. Step 1 is the real path and
it works — data-testid="tier-grid" is live in production. This step produces a
board that does NOT match TierVibe's layout, so reaching for it when Step 1 was
merely not tried gives a visibly worse video:
python <skill_dir>/scripts/render_board.py <work_dir> --width 1920
Builds an APPROXIMATE board (tier labels + card grid) from card images. It will not match TierVibe's exact layout, so prefer Step 1.
Vision-free shortcut: if every card is either a text card or an image card with a non-empty detail (verify in manifest.json), SKIP Steps 4 and 5 - no board_layout.json, no reconcile. Derive each image card's label from its detail (first markdown heading, or the subject of the first sentence; keep it short), write it into manifest.json with label_source: "derived_from_detail", and go straight to Step 6. The API order already equals the board's visual order, so the manifest is already in the right order; build_card_manifest.py and generate_video.py both run fine without reconcile (board_pos just shows -). If even one image card lacks detail, use the vision path below for that card.
View board_hd.png (the whole board at once). In ONE pass, identify every
card in context — tier labels are the row headers, neighbors give context
that an isolated card image lacks. Output the visual layout to
<work_dir>/board_layout.json:
{
"board_title": "编程语言天梯榜 · 2026",
"tiers": [
{ "tier": "T1", "cards": [{"position": 1, "label": "PHP"}, {"position": 2, "label": "C++"}] },
{ "tier": "T2", "cards": [{"position": 1, "label": "C"}, {"position": 2, "label": "Rust"}] }
]
}
Rules:
tier = the tier label as shown on the board (e.g. "T1", or a custom name
like "S"/"夯"). position = 1-based left-to-right index within that tier.label = what the card depicts. If you genuinely cannot read a card on the
board (small/blurry), leave its label empty — Step 5's per-card pass
(higher-res individual image) will fill it.If board_hd.png is missing (Step 1 failed): skip this step, leave
board_layout.json unwritten. Step 5's per-card recognition becomes the only
source; reconcile_cards.py will keep the API order.
manifest.json for card image files in images/.card_XXX.webp (higher-res than the board thumbnail) and confirm
the label. Write it back into each card's label field in manifest.json.
This confirms the board recognition; if it disagrees, that's fine — the
board wins (Step 4 is the truth), but the disagreement is flagged for review.
Exception: text cards. Their label comes from the card's own data
(label_source: "text_card_data"), not from reading an image, so it beats the
board label — reconcile enforces this. Tier and order still come from the
board either way.python <skill_dir>/scripts/reconcile_cards.py <work_dir>
Reads board_layout.json (board truth: tier + order) + manifest.json
(per-card labels), matches each board slot to its card image by label, and
rewrites manifest.json so tiers are in board visual order with each
card tagged board_tier / board_position / matched / label_disagreement.
Card index (the card_XXX file number) is preserved, so narration indices and
the video overlay still map to the right image files. A manifest.pre_reconcile.json
backup is written. If matching drops below 50% (board and API structures diverge
too far), it keeps the API order and warns — review manually instead of
trusting a half-matched reorder.
If a card cannot be recognized on either the board or its individual image, ask the user for that one card rather than guessing — a wrong label misleads the entire narration.
Create <work_dir>/narration_script.json:
{
"title": "中国动画电影龙虎榜",
"language": "zh",
"intro": "大家好,今天来看看中国动画电影的排名...",
"segments": [
{ "index": 0, "tier": "夯", "label": "哪吒之魔童降世",
"narration": "第一名,哪吒之魔童降世。这部电影..." }
],
"outro": "以上就是今天的排名,你觉得合理吗?"
}
Rules:
detail as reference when present. Each card in
manifest.json may carry a non-empty detail - the original author's own
explanation for why that card sits in that tier. If present, ground the
narration in it: prefer the facts/reasons/viewpoints the author wrote,
compressed into spoken-language sentences. If detail is empty, fall back to
a neutral explanation from the card label + tier. This is reference, not
verbatim copy: compress and make it conversational, but do NOT add facts
the author didn't state or contradict their assessment.index must match the card index in manifest.json (unchanged by reconcile).intro and outro are REQUIRED and must be non-empty. If either is
empty, the video's opening/closing title frame will be SILENT (no voiceover)
— the TTS step skips empty text silently and the compose step has no audio
to attach. Always write a 1-2 sentence intro and outro.After the narration is written, generate a human-readable mapping so you and
the user can verify every image file maps to the right card name, tier, board
position, and narration — no "which card_003.png was that?" confusion:
python <skill_dir>/scripts/build_card_manifest.py <work_dir>
Reads manifest.json (file + tier + board_position + board vs card label) +
narration_script.json (label + narration), writes card_manifest.md — one
row per card:
| index | image file | tier | board_pos | card name | detail? | narration (preview) |
|---|---|---|---|---|---|---|
| 0 | card_000.webp | 夯 | 1 | 哪吒之魔童降世 | ✓ | 第一名,哪吒之魔童降世。这部电影... |
Rows where the board recognition and per-card recognition disagreed are shown
as ⚠ board=<x> | card=<y> so you can resolve them. Show this table to the
user in Step 7 review — it's the single source of truth for "which file is
which card". If a row is wrong, fix it in manifest.json (label) or
narration_script.json (narration) and re-run this script. The detail? column marks cards carrying an author detail explanation - at review, check those narrations actually reflect it instead of generic filler.
Present the narration script in readable form; ask the user to confirm or
modify. Revise until approved. Write the final version to narration_script.json.
python <skill_dir>/scripts/tts_narration.py generate <work_dir>/narration_script.json -o <work_dir> [-v VOICE]
Voice by language:
zh-CN-YunxiNeural (male, default) / zh-CN-XiaoxiaoNeural (female)en-US-GuyNeural / en-US-JennyNeuralja-JP-NanamiNeural / ja-JP-KeitaNeuralpython <skill_dir>/scripts/tts_narration.py voices -l <lang_prefix>Generates narration_<NNN>.mp3 per segment PLUS narration_intro.mp3 /
narration_outro.mp3 from the (required, non-empty) intro/outro text. If
intro/outro are empty it warns loudly — fix narration_script.json and re-run.
python <skill_dir>/scripts/generate_video.py <work_dir> -o <output.mp4> [--resolution 1920x1080] [--fps 24] [--scroll-threshold 0.25]
Options:
--resolution 1920x1080 (landscape) or 1080x1920 (vertical/shorts)--intro-duration 3.0 seconds for intro/outro (only used when no intro/outro
audio is attached; with audio, the frame follows the audio's real length)Features:
board_hd.png (Step 1) → server thumb (Step 2) →
board.png (Step 3).manifest.json (board visual order).subtitles.srt alongside the video, timed to the
ACTUAL audio durations (intro/outro/segments) — not a fixed 3.0s guess.intro: attached | outro: attached | cards: N/N with audio); if intro/outro audio is missing it warns — that's the
"silent title frame" failure mode, catch it here.Provide the output video + subtitles file to the user.
Pillow (usually pre-installed)edge-tts (auto-installed; Microsoft online TTS, cross-platform)moviepy>=2.0 + numpy (auto-installed; moviepy bundles ffmpeg)playwright + Chromium (for the high-res board capture; auto-installed on
first run of capture_board.py; first install downloads ~150MB browser)Everything above installs once per Python environment, not once per run: each
script imports first and pip-installs only what's missing, and capture_board.py
downloads Chromium only when it isn't already on disk. A second run — or a fresh
session — reuses what's there. Switching interpreters (a different venv, another
machine) means installing into that environment once.
Cross-platform font detection in the render/compose scripts:
"language" in narration_script.json.generate_video.py now refuses to write that video and says so.
Usually a TEXT-card board fetched before this was supported — re-run
fetch_tierlist.py, which renders text: cards locally. Check its summary
line: Cards: N total — X downloaded, Y text cards rendered, Z FAILED.fit_board()
now scrolls only when the board is >25% taller than the frame and otherwise
fits it whole. Tune with --scroll-threshold; the log says contain (no scroll) or scroll Npx.--resolution 1080x1920) is mostly black: same root cause — a
wide board scaled to 1080 wide is far shorter than 1920, and PIL pads the
out-of-bounds crop with black. Fixed by the same fit_board().--fps 12 roughly halves the time with
no visible difference.capture_board.py says "tier-grid not found": the TierVibe deploy hasn't
shipped the data-testid="tier-grid" attribute yet. Use the 600px thumb
fallback (Step 2) and report it. Board-first recognition (Step 4) then has
no board to read — fall back to per-card-only (Step 5).pip install fails with "externally-managed-environment": PEP 668, on
Debian/Ubuntu system Python or Homebrew Python. The auto-install can't write to
that interpreter. Re-run the same command with pip install --user <pkg>, or
create a venv (python -m venv .venv + activate) and run the scripts from it.detail - derive each label from its detail text (vision-free path, see the "Image-card boards" section above). Only cards with NO detail block you: do NOT fabricate names for them - stop and tell the user to switch to a multimodal model, add a detail (讲解) explanation to those cards in the post, or name them for you.