Turn a 王建硕-style WeChat public account article.md into a 1080×1920 vertical, 30-90 second Chinese narrated short video: TTS voiceover + HyperFrames CSS/GSAP animation + abstract watercolor background + transition SFX. Output MP4 for 视频号 / 抖音 / 小红书 / Reels.
What this skill produces
| Dimension | Default |
|---|---|
| Size | 1080×1920 vertical (9:16) |
| Duration | 30-90 seconds |
| Number of Scenes | 5-10 |
| Voiceover | 火山引擎 Volcano TTS, default 阿虎 conversational male voice |
| Background | Abstract watercolor generated by GPT Image 2 (bg.png) + blur 30 + warm black semi-transparent overlay |
| Font | Noto Sans SC, hero 900, main text warm cream white |
| Output | <article-folder>/<slug>.mp4(parallel to video/ , not placed in video/ ) |
| Publishing | Automatically upload to YouTube — Portrait → Shorts, Landscape → regular video; re-rendering replaces the old video (does not accumulate) |
When this skill fires
- User already has
article.md, says "make it into a video", "make a narration", "narrate it" - User runs
/wjs-converting-text-to-video <article-folder> - User says something like "make videos of all X articles posted yesterday" — batch requests
When NOT to use
- No article draft, just an idea → first use
/wjs-publishing-wechatto write article.md, then come back - What the user wants is subtitle burn-in / translation / voice replacement → use
/wjs-burning-subtitles//wjs-dubbing-video//wjs-localizing-video - Video needs to be English / Spanish or other non-Chinese → this skill focuses on Chinese TTS (Volcano 火山引擎); for non-Chinese use hyperframes' built-in tts command (kokoro English is okay)
- Landscape 16:9 → this skill defaults to vertical; landscape only when the user explicitly requests it
Core Principle
The video is not a visualized reading of the article, but a visual reconstruction of the article.
Each scene is an independent visual moment — a contrast, a parallelism, a number, a metaphor. Text fills the screen, bold sans-serif, key words highlighted in orange. The background is abstract watercolor (softened after blur), and the overall tone is steady, restrained, and impactful.
Rhythm > template. A 5-10 scene video, if it uses the same "two-line contrast" layout from beginning to end, is not a video, it's a slideshow.Modernity comes from contrast — extreme font size differences, asymmetric layouts, alternating short scenes and long scenes, alternating pure-text scenes and geometric-element scenes, alternating watercolor-background scenes and bright punch scenes.
Default is mediocre. If you just pick a few of the easiest from the top of the template table, the result will definitely be a "flat two-line format". Forcefully follow the Step 1b Scene Mix Rule ratio.
Workflow
Step 1: Design 5-10 visual moments
Read <article-folder>/article.md, split into 5-10 scenes according to the argument structure (keep total duration at 30-90 seconds). Short articles (1-2 core points) use 5-6 scenes / 30-50s; long articles use 8-10 scenes / 60-90s. Each scene has one narration segment (voiceover) + one clear visual skeleton.
Template table — 6 categories, 16 types total, mix and match as needed:
A. Hero / Punch (strong-contrast climax, ≥1 per video, duration ≤4s)
| Template | Suitable for |
|---|---|
| A1. Full-screen single-character hero | 1-3 character climax word fills the screen, font size 280-400px |
| A2. Outline hero | Hollow text -webkit-text-stroke: 4px #f5efe5; color: transparent; |
| A3. Color-flip punch | Full-screen background switches to a bright color (orange/red/gold/emerald green, etc.), reversed-out text |
| A4. Gradient text hero | Large text plus background: linear-gradient(...); -webkit-background-clip: text; |
B. Contrast / contrast (contrast structure, 1-2 per piece, duration 5-8s)
| Template | Suitable for |
|---|---|
| B1. Two-line contrast + strikethrough | "Before X, now Y" "Not A, but B" — At most 2 per piece |
| B2. Left-right split-screen contrast | Screen split in two (can add a vertical divider) |
| B3. Diagonal contrast | Top-left ↔ bottom-right, lots of whitespace in the middle |
C. List / structure (multiple parallel items, 1-2 per piece, duration 6-10s)
| Template | Suitable for |
|---|---|
| C1. N cards in a horizontal row | 3-5 parallel items, use deep warm black + single-color borders |
| C2. Vertically stacked keywords | 6-8 parallel items, can add large number labels 01-08 |
| C3. True grid | 2×2 / 3×2 grid, each cell icon + label (portrait width is limited; a 4-column horizontal row will be cramped) |
| C4. Stair-step / staggered list | Each item margin-left increasing |
D. Stat / data (number climax, ≥1 per piece, duration 4-6s)
| Template | Suitable for |
|---|---|
| D1. Number ticker | 0 → N rolling animation (gsap.to({textContent})) |
| D2. Number + label | Main number 200-400px + 60-80px explanation |
| D3. Progress bar / timeline | Horizontal progress bar + nodes |
E. Quote / climax (punchline landing point, 1-2 per piece, duration 6-10s)
| Template | Suitable for |
|---|---|
| E1. Paragraph-level hero text | A 60-100px punchline sentence, left-aligned + emphasis bar on the left |
| E2. Large quotation marks + body text | Huge semi-transparent opening quotation mark as background decoration |
F. Decoration / geometry (rhythm seasoning, optional)
| Template | Suitable for |
|---|---|
| F1. Grid + spinner / progress bar | Multiple concurrent screens |
| F2. Speech bubble ↔ response | Character A says → character B does |
Keep each scene's narration to 3-12 seconds(short punch 3-4s, long breath 10-12s,don't make everything 5-7s). Add up all scenes 30-90 seconds, don't exceed 90 seconds. If the article is short, make it short; 5 scenes × 6s = 30s also qualifies.
Step 1b: Scene Mix Rule (mandatory)
After writing 5-10 scene designs, self-check against the checklist below. If any item is not satisfied → go back and adjust.
Hard ratio rules
- ≥1 type A / type D / type C / type E
- ≤2 B1 templates (two-line strikethrough — historically the most easily abused)
- ≥1 A3 color-flip scene (bright background with reversed-out text)
- ≥4 different template types (at least 4 of A/B/C/D/E/F)
- ≤2 consecutive scenes use the same type
Hard rhythm rules
- scene duration span ≥ 6s (shortest ≤ 4s, longest ≥ 9s)
- ≥2 "short → long → short" or "long → short" rhythm switches
- font size span ≥ 240px (largest hero ≥ 320px, smallest ≤ 80px)
Hard layout rules
- ≥2 scenes not centered (corner-anchored, diagonal, left-aligned, stepped, etc.)
- ≥1 scene with whitespace occupying ≥ 60% of the screen (breathing room)
- ≥1 scene containing geometric decorations (thick lines, color blocks, arrows, dots, large numbers)
Hard color rules
- Most scenes have no
background:color — let the watercolor bg-image show through; only A3 color-flip uses a solid-color bg - color-flip scene colors are not just orange/blue/white (deep red / deep gold / emerald green / pine green / dark purple etc. are all fine)
- emphasis uses at least 2-3 colors (blue for technical terms, gold for value terms, green for growth terms, red for warning terms)
Anti-monotony self-check
- Shrink all scene screenshots into thumbnails and place them side by side — can you tell them apart at a glance? If 8 of them look the same → redo
- Are the visual densities of scenes 1, 4, and 7 different? Some should be dense, some extremely minimal
- Is there a "meta-rhythm"? For example, A opening → 3 B/C developments → D climax → E ending — more dramatic arc than a linear layout
Step 2: Write narration_chunks.json
[
{"id": "s01", "text": "我们以前,是 AI 的领导。现在,我们就是它的维修工。"},
{"id": "s02", "text": "..."}
]
Write narration details:
- More colloquial and more clipped than article.md; use more commas/periods to give TTS natural pauses
- Mixing numbers / English is OK ("Claude Code", "100 times"), Volcano can read it all
- Do not write parenthetical notes, do not write
..., do not write the dash——(TTS will read out the three characters "破折号") - Delete the
**加粗 markdown**from article.md, leaving only plain text - Remove Baixing-related facts: if "百姓网", "百姓网现在 X 人", "百姓网员工", etc. appear in article.md, they must all be stripped or generalized ("百姓网现在 158 个人" → "现实里没几个真人"). This is outdated information and must not go into the video. Likewise, visuals must not contain the "百姓网" label or the "158 人" stat. See [[no-baixing-facts]] for details
Step 3: Generate TTS narration
cd <article-folder>/video
python3 tts_narration.py
The script uses zh_male_ahu_conversation_wvae_bigttsby default (Ahu dialogue) — insert 0.35s of silence between paragraphs, output narration.mp3 + timing.json。
Volcano TTS notes (pitfalls encountered):
- Use resource
volc.service_type.10029, choose speakerzh_*_*_bigtts - Never pass
emotion/emotion_scale— most_bigttsvoices will returndata: nullsilent failure - Never use kokoro(hyperframes built-in tts) — poor Chinese quality, explicitly unacceptable to the user
- Avoid
zh_male_jieshuonansheng_mars_bigtts— containing English proper nouns (such as "Claude Code") will cause looping hallucination
Backup voices(in recommended order):
zh_male_ahu_conversation_wvae_bigtts(Ahu dialogue) — default, natural colloquialzh_male_M392_conversation_wvae_bigtts— same wvae serieszh_male_wennuanahu_moon_bigtts(Warm Ahu) — warmer, broadcast feelzh_male_silang_mars_bigtts(Silang) — calm and thoughtful, strong dramatic feelzh_male_baqiqingshu_mars_bigtts(Baqi) — more forceful
Switch voice:python3 tts_narration.py --voice zh_male_silang_mars_bigtts
Step 4: Generate watercolor background images
bg-image is the main visual tone (softened abstract watercolor).Do not use article's illustration.png — hand-drawn diagram has too much detail; after blur it turns into uniform dark mud (visually still pure black). Must use specially generated abstract watercolor.
~/.claude/skills/wjs-converting-text-to-video/scripts/generate-bg.sh <article-folder> <theme>
<theme> Choose (based on article theme):
| theme | Palette | Suitable for |
|---|---|---|
personal |
bright warm yellow, soft coral pink, terracotta, sage green, cream | Personal, handmade, warm |
tech |
cool teal, electric blue, deep purple, mint, white | AI, technology, data |
reflection |
sage green, dusty blue, lavender, pearl, cream | Reflection, calm |
warning |
burnt orange, deep red, mustard, charcoal | Warning, tension |
growth |
fresh green, gold, soft yellow, sky blue | Growth, compounding |
abstract |
lavender, dusty rose, sage, soft amber | Abstract, philosophical |
Output:<article-folder>/video/bg.png (1088×1920, ~3MB)。
⚠️ Images must be inside the video/ directory — cannot use ../illustration.png, hyperframes render does not resolve cross-directory relative paths, and will render as pure black.
Step 5: Write HyperFrames composition (index.html)
Read timing.json, design scenes according to each chunk's start/end. Vertical 1080×1920 structure:
<html><head><script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
html, body {
width: 1080px; height: 1920px; margin: 0; overflow: hidden;
background: #0e0b08;
font-family: 'Noto Sans SC', 'PingFang SC', 'Heiti SC', sans-serif;
font-weight: 900;
color: #f5efe5;
letter-spacing: -0.02em;
-webkit-font-smoothing: antialiased;
}
#bg-image {
position: absolute; inset: 0;
background-image: url('bg.png');
background-size: cover;
background-position: center;
filter: blur(30px) brightness(0.65) saturate(0.85);
z-index: 0;
transform: scale(1.1);
}
#bg-overlay {
position: absolute; inset: 0;
background: rgba(14, 11, 8, 0.28);
z-index: 1;
}
.scene { position: absolute; inset: 0; overflow: hidden; opacity: 0; z-index: 2; }
#s1 { opacity: 1; }
/* ... scene-specific styles ... */
</style></head>
<body>
<div id="root" data-composition-id="main" data-start="0" data-duration="<total+2>" data-width="1080" data-height="1920">
<div id="bg-image"></div>
<div id="bg-overlay"></div>
<!-- scene divs s1..sN -->
<!-- audio: narration + ticks + chimes + bell -->
</div>
<script>
/* GSAP timeline: paused + register to window.__timelines['main'] */
</script>
</body></html>
🎬 First-frame rules (hard requirements)
Video t=0 must include:
- bg-image fully visible — always opacity 1, never fade-in (visible by CSS default, don't change its opacity in GSAP)
- Title element visible — s1's main title element
tl.from({y:30, scale:0.95})is OK, but do nottl.from({opacity:0}), otherwise t=0 is a black screen - s1 cannot be A3 color-flip — otherwise it covers the bg-image, and the watercolor is not visible on the first frame. Leave color-flip for s2+
Color system
Main text / anchor color (design system, consistent throughout):
| Role | Value | Usage |
|---|---|---|
| Main text | #f5efe5 warm cream white |
hero / main content |
| Secondary text (subtitle, caption) | #f5efe5 + opacity: 0.7 + small font size |
Do not use gray(#8a7e72 it is hard to see on the watercolor background). Use opacity + reduced font size for hierarchy |
| Strikethrough text itself | #f5efe5 + opacity: 0.5 + strikethrough line |
Do not use #6d635a dark gray — it is hard to see on the watercolor background. Instead use reduced opacity + orange strike line |
| Decorative large numbers (01-08) | #f5efe5 + opacity: 0.18 or #e87a3e + opacity: 0.35 |
Do not use #2b2620 and other dark grays(they completely disappear on the watercolor background) |
| Outline stroke | #f5efe5 4-8px stroke + color: transparent |
A2 hollow text |
| Default fallback bg | #0e0b08 dark warm black |
Covered by bg-image + overlay; color-flip does not use it |
Core principle: all text uses #f5efe5 cream or #e87a3e orange family (accent palette), use opacity + size for hierarchy, no hue changes. Gray is a relic of the black-background era, never use it on the watercolor background. See [[no-low-contrast-text]] for details
Color-flip background palette (A3, not just orange/blue/white):
| hex | Suitable for |
|---|---|
#e87a3e classic orange |
Warning, emphasis, climax punch |
#6b9bc4 bright blue |
Data, technology climax |
#f5efe5 warm cream white |
Ending, quiet contrast |
#c45c3e Deep red |
Warning, error climax |
#d4a040 Deep gold |
Achievement, value climax |
#5a8c6a Emerald green |
Growth, compounding, vitality |
#4a8a8a Teal |
Calm, long-termism |
#7a5a8a Dark purple |
Wisdom, mystery climax |
#c48a8a Dusty pink |
Softness, humanity |
Text on color-flip uses #0e0b08 or #f5efe5 inverted.
Emphasis / Accent palette (not just orange):
| hex | Suitable for |
|---|---|
#e87a3e Orange |
Default emphasis |
#6b9bc4 Blue |
Data, technical terms, AI |
#d4a040 Gold |
Value, achievement |
#5a8c6a Emerald green |
Growth, good results |
#4a8a8a Teal |
Long-term, stable |
#c45c3e Deep red |
Warning, contrast |
#8a7aaa Dark purple |
Abstract, wisdom |
#c48a8a Dusty pink |
Softness, human touch |
Entire emphasis ≥ 2-3 types, choose accent based on scene theme.
Font system (Vertical 1080 width)
| Item | Value |
|---|---|
| Weight | hero 900 / body 800 / secondary 600-700 / caption 500 |
| Letter spacing | hero -0.04em to -0.06em / body -0.02em / caption 0 |
| Punch hero (A1/A2, 1-3 characters) | 280-400px |
| Short-sentence hero (4-6 characters) | 160-240px |
| Long-sentence hero (7-10 characters) | 100-150px |
| Card content | 56-130px |
| Subtitle | 40-72px |
| Caption / number / label | 20-40px |
Layout system (Anti-centering inertia)
| Layout | CSS key | Suitable for |
|---|---|---|
| Centered | flex; center; center; |
Type A hero, but ≤50% scene |
| Left-aligned top-anchored | padding: 80px 80px 0 80px; |
Type E quote, long quote |
| Bottom-right anchored | position: absolute; right: 80px; bottom: 80px; |
Signature, climax word |
| Diagonal | top-left / bottom-right | B3 diagonal contrast |
| Grid | display: grid; grid-template-columns: repeat(2, 1fr); |
C3 (vertical 2×N rather than 3×N) |
| Staircase | Each item margin-left: calc(60px * var(--i)); |
C4 staggered list |
| Bottom-anchored + blank space above | position: absolute; bottom: 60px; blank space above |
Breathing scene |
| Small corner element | Text in a small corner, everything else empty | Minimalist / whitespace punch |
Padding: full-bleed type 40-80px, breathing type 120-200px. Don't use the same padding for every scene.
Geometric decorative elements
Use one every few scenes:
- Thick short line 8-16px × 40-200px, emphasis bar, orange
- Left emphasis bar 6px × 100%, with long quote
- Large number label 01-08, list item numbers (light gray, huge, decorative)
- Large quotation mark character
"Semi-transparent oversized placed top-left - Horizontal divider 2-4px off-white 30% transparent
- Dot / square 12-20px, orange, list bullet
- Arrow ➜ or custom-drawn SVG
Scene transitions (4 types + mixing rules)
Don't use blur crossfade for the whole video. Every 4 transitions must have ≥2 types.
T1. Blur crossfade(default soft)
- 0.6s,
sine.inOut - previous scene
opacity: 0, filter: blur(24px)→opacity: 1, filter: blur(0) - next scene fades out + blurs simultaneously
T2. White flash cut(punch cut, most modern)
- 0.18s total: 60ms white flash → cut → 40ms new scene scale 1.05 → 1
- Suitable for: entering type A hero, type D stat, climax transitions
tl.to('.flash', { opacity: 1, duration: 0.06, ease: 'none' }, T - 0.06)
.set(prevScene, { opacity: 0 }, T)
.set(nextScene, { opacity: 1 }, T)
.to('.flash', { opacity: 0, duration: 0.12, ease: 'power2.out' }, T)
.from(nextScene, { scale: 1.05, duration: 0.25, ease: 'expo.out' }, T);
T3. Scale push(sense of push)
- 0.55s, previous scene
scale: 1 → 0.85, next scenescale: 1.15 → 1 - Suitable for: pushing from overview to details
T4. Color flash cut(flash orange/blue once, strong rhythm)
- 0.22s total: 80ms full-screen orange → cut → 40ms retract
- Suitable for: entering A3 color-flip or a key turning point
- At most 2 times in the whole video
Add flash overlay in HTML <div class="flash"> full-screen positioning, default opacity 0, z-index 100.
Entrance animation rules
- Every element of every scene uses
tl.from(...)entrance (y/opacity/scale) - Entrance stagger 0.1-0.3s; first element t = scene.start + 0.3 onward
- ≥3 different eases (
power3.out/back.out(1.3)/expo.out/elastic.out(1, 0.5)) - Don't
gsap.to({opacity: 0})exit — transitions already handle it. Only the last scene may fade-to-black - The whole video must use ≥3 types Modern Motion Techniques
Modern Motion Techniques
Half the difference between mediocre videos and modern videos is in typography, half in motion. Each video below must use ≥3 of these 7 types (used for specific scenes, don't pile them all on).
1. Kinetic Typography (character stagger entrance) —— type A hero
<h1 class="kinetic">维 修 工</h1>
tl.from('.kinetic span', {
y: 180, opacity: 0, rotateX: -90,
duration: 0.7, stagger: 0.06,
ease: 'back.out(1.4)',
transformOrigin: '50% 100%',
}, T);
2. Camera Punch (push in / pull out) —— A3, type D
tl.from(scene, { scale: 1.15, opacity: 0, duration: 0.5, ease: 'expo.out' }, sceneStart);
3. Mask Reveal (clip-path reveal) —— type E quote
.reveal { clip-path: inset(0 100% 0 0); }
tl.to('.reveal', { clipPath: 'inset(0 0% 0 0)', duration: 0.9, ease: 'expo.inOut' }, T);
4. Number Ticker (number rolling) —— D1
<div class="ticker" data-end="3600">0</div>
const ticker = document.querySelector('.ticker');
const obj = { val: 0 };
tl.to(obj, {
val: parseInt(ticker.dataset.end),
duration: 1.8, ease: 'power2.out',
onUpdate: () => { ticker.textContent = Math.round(obj.val).toLocaleString(); },
}, T);
5. Outline → Fill (outline text becomes solid) —— A2
.morph { -webkit-text-stroke: 4px #f5efe5; color: transparent; }
tl.to('.morph', { color: '#e87a3e', webkitTextStrokeColor: '#e87a3e', duration: 0.5, ease: 'power2.out' }, T);
6. Letter Highlight Sweep (keyword light sweep) —— type E climax word
<span class="sweep"><span class="sweep-bg"></span>搭档</span>
.sweep { position: relative; display: inline-block; padding: 0 8px; }
.sweep-bg { position: absolute; inset: 0; background: #e87a3e; transform: scaleX(0); transform-origin: left; z-index: -1; }
tl.to('.sweep-bg', { scaleX: 1, duration: 0.5, ease: 'power3.inOut' }, T);
tl.to('.sweep', { color: '#0e0b08', duration: 0.1 }, T + 0.25);
7. Background Color Punch (background flash change) —— 1-2 times in the whole video
tl.to(scene, { backgroundColor: '#e87a3e', duration: 0.08 }, T)
.to(scene, { backgroundColor: '#0e0b08', duration: 0.4, ease: 'power2.out' }, T + 0.1);
Strike-through animation: use real DOM <span class="strike-line"> instead of ::after. Pseudo-elements + CSS variables don't work in some rendering paths of hyperframes.
<span class="strike">领导<span class="strike-line"></span></span>
.strike-line { position: absolute; left: -10px; right: -10px; top: 56%; height: 10px; background: #e87a3e; transform: scaleX(0); transform-origin: left; }
tl.to('.strike .strike-line', { scaleX: 1, duration: 0.55, ease: 'power2.inOut' }, T);
Step 6: Add SFX
~/.claude/skills/wjs-converting-text-to-video/scripts/synth-sfx.sh <article-folder>/video
Generate video/sfx/{tick,chime,bell}.mp3:
tick.mp3— 80ms 1.2kHz sine,for transitions(0.3s before each scene switch)chime.mp3— 220ms 880+1320Hz dual tone, when dialogue/list item lights up (optional)bell.mp3— 1.5s low-frequency bell, when the final climax word appears (at most 1 time in the whole video)
Connect to timeline:
<audio id="aud-narration" src="narration.mp3" data-start="0" data-duration="<total>" data-track-index="0" data-volume="1"></audio>
<audio id="aud-tick-s02" src="sfx/tick.mp3" data-start="<scene2.start - 0.3>" data-duration="0.1" data-track-index="2" data-volume="0.55"></audio>
<!-- 重复每个 scene 切换;T2/T4 flash 转场可不加 tick -->
<audio id="aud-chime-s08-1" src="sfx/chime.mp3" data-start="<T>" data-duration="0.3" data-track-index="3" data-volume="0.45"></audio>
<audio id="aud-bell-s12" src="sfx/bell.mp3" data-start="<climax-T>" data-duration="1.6" data-track-index="4" data-volume="0.55"></audio>
⚠️ Every <audio> must have id, otherwise it renders silent (mandatory requirement of hyperframes).
Different track-index do not conflict, the same track cannot overlap in time.
Use SFX sparingly: transition tick is mandatory; chime / bell are decorative, don't add them when scene content is simple; bell only 1 time in the whole video.
Step 7: Lint + Inspect + Render (must be in order)
cd <article-folder>/video
# 必跑 1:linter(必须 0 errors)
npx hyperframes lint
# 必跑 2:layout inspect 找溢出(必须 0 errors)
npx hyperframes inspect --at 1,8,15,25,35,45,55,65
# 推荐:snapshot 看排版
npx hyperframes snapshot --at <t1>,<t2>,<t3> .
# 渲染(lint + inspect 都通过才能跑)
# ⚠️ 输出到上级目录,与 video/ 平行 —— 最终 MP4 不放 video/ 里
npx hyperframes render --quality standard --fps 30 --output ../<slug>.mp4
Why inspect must be run: In portrait mode, 1080 wide is very narrow; a 3-4 character hero at 280-400px font size is close to overflowing. Every time you must inspect,0 errors before it can render。
fix overflow:
- reduce font size (inspect gives specific suggestions)
- split long hero into lines ("can't accumulate" → two lines "can't" / "accumulate")
white-space: nowraponly when confirming character count × font size < screen width- If
.emoverflows withinreveal-wrap→ addline-height: 1to.em
render quality:
--quality draft~30s render — use for iteration--quality standard~1.5min — default, good enough for publishing--quality high~3min — for large screens / business
Step 8: Preview
Output:<article-folder>/<slug>.mp4(parallel to video/ , not inside, not within video/ —— video/ reserved for intermediate files).
open <article-folder>/<slug>.mp4 for user preview.Do not auto-upload to WeChat Channels(the user may want to edit/adjust first).
Step 9: Publish to YouTube (automatic cron,not within the render flow)
after a new video render completesdo not upload immediately —— YouTube has daily quota limits (default 6/day @ 1600 quota units/upload); rendering too many will hit the quota limit.
Approach: cron automatically runs daily at 10:00 daily-upload-batch.sh, picks at most 5 MP4s that have not been uploaded yet (in ascending order of article date), and after uploading writes .youtube.json record.
cron already registered(one-time, no need to run repeatedly):
0 10 * * * /Users/jianshuo/.claude/skills/wjs-converting-text-to-video/scripts/daily-upload-batch.sh
Manual trigger(Do not run within the wjs-converting-text-to-video flow — let cron handle it):
~/.claude/skills/wjs-converting-text-to-video/scripts/daily-upload-batch.sh
# 或单个文章立即上传
~/.claude/skills/wjs-converting-text-to-video/scripts/publish-to-youtube.py <article-folder>
Behavior of each upload script:
- Detect MP4 portrait/landscape → add to portrait title
#shorts, landscape normal video - title from article.md H1 / description from the first few paragraphs
- Check
<article-folder>/.youtube.json: exists → try to delete the old one then upload the new one (requiresyoutube.force-sslscope; the current token doesn't have this scope → skip delete + upload new) - Write
.youtube.jsonrecord
See memory: [[auto-publish-youtube]]
Directory structure
<article-folder>/
├── article.md
├── illustration.png # 用户原始示意图,不直接用作 bg
├── <slug>.mp4 # ⭐ 最终视频(与 video/ 平行,不放 video/ 里)
└── video/ # 所有中间产物
├── narration_chunks.json # 5-10 个 scene 的旁白文本
├── tts_narration.py # bootstrap 复制进来
├── narration.mp3 # 合并的全段 TTS
├── narration/ # 单段 mp3 (s01..sN)
├── timing.json # 每段 start/end/duration
├── bg.png # GPT Image 2 生成的水彩背景
├── sfx/{tick,chime,bell}.mp3
├── index.html # HyperFrames composition
├── hyperframes.json
├── meta.json
├── package.json
└── snapshots/ # 渲染前快照
Skill's own files
~/.claude/skills/wjs-converting-text-to-video/
├── SKILL.md
└── scripts/
├── bootstrap-project.sh # init video/ 目录 + 复制 helper + 生成 sfx
├── generate-bg.sh # 调 GPT Image 2 生成抽象水彩 bg.png
├── tts.py # Volcano TTS narration 生成
├── synth-sfx.sh # tick/chime/bell 合成 (ffmpeg)
├── retrofit-bg-image.py # 给已有视频补 bg-image 层
├── strip-dark-scene-bgs.py # 剥离 scene-level 暗色 bg,让 bg-image 透出
└── publish-to-youtube.py # 自动上传 MP4 到 YouTube(portrait→Shorts),可替换已有上传
Anti-Patterns
Anti-monotony (most important — the root of "flat, straightforward narration")
| Don't | Reason |
|---|---|
| All scenes use B1 two-line strikethrough | The biggest historical failure mode. B1 at most 2 times in the whole video |
| All scenes use centered layout | Dead and lifeless. ≥2 non-centered |
| All scenes have similar font sizes | Span must be ≥240px |
| All scenes are 5-7s long | Span must be ≥6s |
| The whole video uses only blur crossfade | Every 4 transitions ≥2 types |
| The whole video has no color-flip | ≥1 A3 is a hard requirement |
| The whole video has no geometric elements | ≥1 scene with bold line / large number / quotation mark |
The whole video uses only tl.from({y, opacity}) |
≥3 Modern Motion Techniques |
| Every scene is packed full | ≥1 scene with ≥60% whitespace |
Add to every scene background: color |
covers the bg-image, which is equivalent to generating watercolor for nothing. Regular scenes do not write bg; only A3 color-flip uses a solid color |
| color-flip / emphasis always use only orange | at least 2-3 kinds of accent |
| use gray for secondary text / strike / decoration | on a watercolor background, gray has too low contrast and will disappear. Switch to #f5efe5 cream + opacity to weaken it (see [[no-low-contrast-text]] for details) |
Content / Engineering
| Do not | Reason |
|---|---|
| Use Kokoro for Chinese TTS | Chinese quality is poor, and the user explicitly does not accept it |
Volcano TTS passes emotion parameter |
_bigtts voice returns data: null silent failure |
Use zh_male_jieshuonansheng_mars_bigtts |
when it contains English proper nouns, it loops and hallucinates |
| Use a serif font (Songti / SimSun / Noto Serif) | not impactful enough |
| paste the entire article on screen | that is a PPT. A video has one visual moment per screen |
| more than 10 scenes / more than 90 seconds | attention cannot hold it |
| force a short article to fill 90 seconds | if the article is short, make it 30-50s; forcing it longer will pad it and make it shallow |
| change the font, color scheme, and style for every scene | style drift. The design system is fixed; templates vary |
::after pseudo-element + CSS variables for strike |
fails under the hyperframes rendering path. Use real DOM <span class="strike-line"> |
outside the last scene, use gsap.to({opacity: 0}) |
exit animations are forbidden in hyperframes — transitions are the exit |
| add a chime to every chunk | too noisy |
Use ../illustration.png as the bg url |
hyperframes render does not resolve cross-directory paths and renders pure black.bg.png must be inside video/ inside |
<audio> has no id |
render will be silent. Every <audio> must id="..." |
| s1 is A3 color-flip | the bg-image is not visible on the first frame. Put color-flip in s2+ |
all title elements in s1 are from({opacity: 0}) |
black screen on the first frame. The main element in s1 opacity: 1 by default, only animate y/scale |
Common Pitfalls
- changed the narration text but TTS did not change (cache was used) →
tts_narration.pyby chunkidcaches a single-segment mp3; if the copy is changed butidhas not changed, the old audio will be reused (symptom:timing.jsonduration is exactly the same as the previous version even though the text got longer). Before re-synthesizing, firstrm -rf narration/ narration.mp3 timing.jsonand run again - narration writes an em dash "——" → TTS reads out "em dash". Delete it and use a period or comma
- a certain chunk is abnormally long (>3 chars/s) → Volcano hallucination loop. Change the voice, or split it shorter
- scene duration < narration duration → the narration is cut off by the next scene. The scene must cover the entire narration + 0.3s buffer
- large text on a black background is still visible when opacity: 0 → check whether
.scenehasopacity: 0by default (except s1) .emin.reveal-wrapslight overflow in (top/bottom a few px) → give.emaddline-height: 1- snapshot glyphs and render inconsistent → now all use Noto Sans SC, normally consistent
Dependencies
- HyperFrames CLI (
npx hyperframes) — composition lint / inspect / snapshot / render - GPT Image 2 (
~/.claude/skills/gpt-image-2-skill/) — generate bg.png;--provider codexuse ChatGPT auth - Volcano TTS —
VOLC_TTS_APPID/VOLC_TTS_ACCESS_TOKENin~/code/.env - ffmpeg — SFX synthesis, audio concat, aspect-ratio detection
- YouTube uploader (
~/.claude/skills/wjs-uploading-video/) + OAuth token at~/.config/youtube/token.json—— Step 9 auto-publish