TierList Video Maker
Turn any published TierVibe tier list into a narrated video with a scrolling high-resolution background.
Transparent image handling
Video composition preserves PNG/WebP alpha for card overlays. Transparent card images are resized with alpha-aware premultiplication and composited on an integrated semi-transparent dark glass backing with a soft dark edge, so cutout/standee images stay readable without black halos or a harsh rectangular box.
⚠️ Only published posts — drafts / still-editing posts cannot be used
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.
⚠️ Image-card boards need multimodal vision — text-card boards don't
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 astext:<urlencoded label>#<fg>#<bg>);fetch_tierlist.pyhas already rendered the swatch and filledlabel, tagging itlabel_source: "text_card_data". Skip the vision work in Steps 4 and 5 — read the labels straight frommanifest.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'sdetail:- Image card WITH a non-empty
detail- vision-free. The author's commentary names or clearly references the item, so you can DERIVE a short card label from thedetailtext (e.g. its first markdown heading, or the subject of the first sentence) without looking at the image. Write that label into the card'slabelfield withlabel_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. - Image card WITHOUT
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.
- Image card WITH a non-empty
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.
How the board image is obtained (read this first)
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.
Workflow (follow in order)
Step 1 — Capture the high-resolution board (visual source of truth)
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.
Step 2 — Fetch tier list data + card images
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.
Step 3 — (Last resort) Render an approximate board
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.
Step 4 — Board-first recognition (produce board_layout.json)
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:
- Visual reading order: top tier → bottom tier, within each tier left → right.
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 itslabelempty — Step 5's per-card pass (higher-res individual image) will fill it.- This whole-board pass is the primary recognition. It is more accurate than per-card because the board carries tier + neighbor context.
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.
Step 5 — Per-card confirmation + reconcile
- Read
manifest.jsonfor card image files inimages/. - View each
card_XXX.webp(higher-res than the board thumbnail) and confirm the label. Write it back into each card'slabelfield inmanifest.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. - Reconcile the two:
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.
Step 6 — Generate narration script
Create <work_dir>/narration_script.json:
{
"title": "中国动画电影龙虎榜",
"language": "zh",
"intro": "大家好,今天来看看中国动画电影的排名...",
"segments": [
{ "index": 0, "tier": "夯", "label": "哪吒之魔童降世",
"narration": "第一名,哪吒之魔童降世。这部电影..." }
],
"outro": "以上就是今天的排名,你觉得合理吗?"
}
Rules:
- Match the language of the tier list title (Chinese title → Chinese narration).
- Each segment: 1-3 sentences, concise and engaging.
- Group by tier: introduce each tier before its cards.
- Use the author's
detailas reference when present. Each card inmanifest.jsonmay carry a non-emptydetail- 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. Ifdetailis 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. indexmust match the card index inmanifest.json(unchanged by reconcile).introandoutroare 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.
Step 6½ — Generate the card manifest table (file ↔ name ↔ tier ↔ narration)
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.
Step 7 — User review
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.
Step 8 — Generate TTS audio
python <skill_dir>/scripts/tts_narration.py generate <work_dir>/narration_script.json -o <work_dir> [-v VOICE]
Voice by language:
- Chinese:
zh-CN-YunxiNeural(male, default) /zh-CN-XiaoxiaoNeural(female) - English:
en-US-GuyNeural/en-US-JennyNeural - Japanese:
ja-JP-NanamiNeural/ja-JP-KeitaNeural - List all:
python <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.
Step 9 — Compose video
python <skill_dir>/scripts/generate_video.py <work_dir> -o <output.mp4> [--resolution 1920x1080] [--fps 24] [--scroll-threshold 0.25]
Options:
--resolution 1920x1080(landscape) or1080x1920(vertical/shorts)--intro-duration 3.0seconds for intro/outro (only used when no intro/outro audio is attached; with audio, the frame follows the audio's real length)
Features:
- Background priority:
board_hd.png(Step 1) → server thumb (Step 2) →board.png(Step 3). - Card order follows the reconciled
manifest.json(board visual order). - Scrolling background: if the board is taller than the frame, scrolls top→bottom.
- Vivid board background: the board stays full-color and SHARP during card narration - NEVER blur or darken it. The colorful board is the attraction; the card's own dark backing keeps it readable. (A blurred mid-frame had no pull.)
- Card overlay: each card zoomed to center with a tier badge + label.
- Subtitles: auto-generates
subtitles.srtalongside the video, timed to the ACTUAL audio durations (intro/outro/segments) — not a fixed 3.0s guess. - Prints an audio summary at start (
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. - Cross-platform: Windows / macOS / Linux.
Step 10 — Deliver
Provide the output video + subtitles file to the user.
Dependencies
- Python 3.10+
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 ofcapture_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:
- Windows: Microsoft YaHei (msyh.ttc)
- macOS: PingFang SC
- Linux: Noto Sans CJK
Multi-language
- Set
"language"innarration_script.json. - Choose a matching TTS voice.
- Subtitles are generated in the narration language.
- Card labels and tier names are preserved as-is.
Troubleshooting
- No card images: the URL must be a published (not draft) post.
- The video is only an intro and an outro, nothing in between: no card had an
image on disk.
generate_video.pynow refuses to write that video and says so. Usually a TEXT-card board fetched before this was supported — re-runfetch_tierlist.py, which renderstext:cards locally. Check its summary line:Cards: N total — X downloaded, Y text cards rendered, Z FAILED. - The board is cut off at the bottom (or the title bar is cut off at the top):
older builds always scaled the board to frame width and cropped, so a board only
slightly taller than the frame silently lost its last tier row.
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 sayscontain (no scroll)orscroll Npx. - Portrait (
--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 samefit_board(). - Rendering takes far longer than the video is: every clip is a still frame,
so most rendered frames are identical.
--fps 12roughly halves the time with no visible difference. capture_board.pysays "tier-grid not found": the TierVibe deploy hasn't shipped thedata-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 installfails 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 withpip install --user <pkg>, or create a venv (python -m venv .venv+ activate) and run the scripts from it.- TTS fails: needs network; edge-tts uses Microsoft's online service.
- Video encoding fails: moviepy bundles ffmpeg; if issues, install ffmpeg.
- Font missing: scripts fall back to a default font; install Noto Sans CJK on Linux.
- Card unclear: ask the user to help identify specific cards.
- Model can't recognize images: if you (the running model) have no image recognition, you can STILL finish the video as long as every image card has a non-empty
detail- derive each label from itsdetailtext (vision-free path, see the "Image-card boards" section above). Only cards with NOdetailblock you: do NOT fabricate names for them - stop and tell the user to switch to a multimodal model, add adetail(讲解) explanation to those cards in the post, or name them for you.
评论
加载中…