SuperAPI AI Directory

wjs-converting-text-to-video

Design & creation

View repository

Updated 10-15 23:32

About this skill

wjs-converting-text-to-video

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-wechat to 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

  1. 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
  2. Are the visual densities of scenes 1, 4, and 7 different? Some should be dense, some extremely minimal
  3. 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 speaker zh_*_*_bigtts
  • Never pass emotion / emotion_scale — most _bigtts voices will return data: null silent 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 colloquial
  • zh_male_M392_conversation_wvae_bigtts — same wvae series
  • zh_male_wennuanahu_moon_bigtts (Warm Ahu) — warmer, broadcast feel
  • zh_male_silang_mars_bigtts (Silang) — calm and thoughtful, strong dramatic feel
  • zh_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:

  1. bg-image fully visible — always opacity 1, never fade-in (visible by CSS default, don't change its opacity in GSAP)
  2. Title element visible — s1's main title element tl.from({y:30, scale:0.95}) is OK, but do not tl.from({opacity:0}), otherwise t=0 is a black screen
  3. 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 scene scale: 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: nowrap only when confirming character count × font size < screen width
  • If .em overflows within reveal-wrap → add line-height: 1 to .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:

  1. Detect MP4 portrait/landscape → add to portrait title #shorts, landscape normal video
  2. title from article.md H1 / description from the first few paragraphs
  3. Check <article-folder>/.youtube.json: exists → try to delete the old one then upload the new one (requires youtube.force-ssl scope; the current token doesn't have this scope → skip delete + upload new)
  4. Write .youtube.json record

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.py by chunk id caches a single-segment mp3; if the copy is changed but id has not changed, the old audio will be reused (symptom:timing.json duration is exactly the same as the previous version even though the text got longer). Before re-synthesizing, first rm -rf narration/ narration.mp3 timing.json and 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 .scene has opacity: 0 by default (except s1)
  • .em in .reveal-wrap slight overflow in (top/bottom a few px) → give .em add line-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 codex use ChatGPT auth
  • Volcano TTS — VOLC_TTS_APPID / VOLC_TTS_ACCESS_TOKEN in ~/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