Drop in an article, get a 9:16 video.
One command, no editing, reproducible every single time.
🌐 English · Tiếng Việt
Quick Start · How It Works · Installation · Usage · TemplatesWhy you can trust it: The AI only handles content (writing the script, picking templates), while the code handles visuals (pixel by pixel). That means the same
script.jsonalways produces the exact same video — no guesswork, no manual touch-ups.
You only worry about the words. Templates handle all the design, layout, and motion; the pipeline handles TTS, audio, rendering, and muxing — delivering three ready-to-use files for CapCut / TikTok / Shorts / Reels:
| File | Purpose |
|---|---|
video.mp4 |
Complete 9:16 video with voiceover + SFX |
voice.mp3 |
Isolated voiceover track — drag straight into CapCut |
script.txt |
Raw transcript — for CapCut auto-caption |
git clone git@github.com:algodaohub/VidFrame.git
cd VidFrame
npm install
cp .env.example .env.local # set TTS_PROVIDER and matching API key|
With Claude Code / OpenCode / Agents CLI — recommended The agent fetches the content, writes |
Manual — bring your own npm run pipeline -- output/my-video/script.jsonFull control over every scene and template. |
A few minutes later → output/<slug>/video.mp4 (1080×1920).
flowchart LR
A["📰 URL / .txt / video"] -->|/create-template-video| B[AI Agent]
B -->|read + write text| C["script.json\nrenderer: hyperframes"]
C -->|Zod validate| D[Template Pipeline]
D -->|TTS per scene| E[TTS Provider]
E -->|atempo speed| F1[Speed adjust]
F1 -->|merge + mix SFX| F[voice.mp3]
D -->|render each template| G["HyperFrames\nChromium"]
G -->|fit clip to voice| H["clips/scene-*.mp4"]
F --> I[mux audio]
H --> I
I -->|🎬| J["video.mp4\n1080×1920"]
style A fill:#0f172a,color:#fff,stroke:#334155
style B fill:#6366f1,color:#fff,stroke:#6366f1
style E fill:#f59e0b,color:#fff,stroke:#f59e0b
style G fill:#ec4899,color:#fff,stroke:#ec4899
style J fill:#10b981,color:#fff,stroke:#10b981
The pipeline runs 8 deterministic steps — source at src/render/template-pipeline.ts:
| # | Step | Output |
|---|---|---|
| 1 | Validate | script.json checked against Zod schema |
| 2 | Caption text | script.txt — all voiceText joined (CapCut auto-caption) |
| 3 | TTS / scene | voice/scene-<id>.mp3 via TTS provider (idempotent) |
| 4 | Speed up | atempo FFmpeg adjusts speed × TTS_SPEED (default 1.15×) |
| 5 | Merge voice | voice-raw.mp3 with 0.3s gaps + per-scene time markers |
| 6 | Mix SFX | voice.mp3 — SFX layered on top of the voiceover |
| 7 | Render clip | clips/scene-<id>-fit.mp4 — template → MP4, fitted to voice length |
| 8 | Mux | video-silent.mp4 → video.mp4 (audio merged in) |
System Requirements
| Requirement | Version | Notes |
|---|---|---|
| Node.js | ≥ 22 | node --version |
| FFmpeg + ffprobe | latest | must be on PATH (ffmpeg -version) |
| Chrome / Chromium | any | HyperFrames uses it to render each template |
| yt-dlp | optional | Required for YouTube / TikTok / Facebook URL input |
| OmniVoice server | optional | Local TTS at OMNIVOICE_ENDPOINT (default http://127.0.0.1:8123) |
| Claude Code / OpenCode / Agents CLI | optional | Only needed for the /create-template-video skill |
Install FFmpeg:
- macOS —
brew install ffmpeg - Windows —
winget install Gyan.FFmpeg - Linux —
sudo apt install ffmpeg
Install yt-dlp (only if you need YouTube/TikTok input):
- macOS —
brew install yt-dlp - Windows / Linux —
pip install yt-dlp
Configuration — .env.local
Copy the sample file and fill in your credentials:
cp .env.example .env.localContents of .env.local:
# ─── Active TTS Provider ──────────────────────────────────────────────────────
# Choose one of: omnivoice | google | openai | elevenlabs
TTS_PROVIDER=omnivoice
# ─── Reading Speed ────────────────────────────────────────────────────────────
# 1.0 = normal · 1.15 = slightly faster (default) · 1.3 = noticeably fast
TTS_SPEED=1.15
# ─── TTS Scene Concurrency ────────────────────────────────────────────────────
# Keep at 1 for local OmniVoice; can increase to 3–5 for cloud APIs
TTS_CONCURRENCY=1
# ─── OmniVoice (local, no API key needed) ────────────────────────────────────
OMNIVOICE_ENDPOINT=http://127.0.0.1:8123
# ─── Google TTS (Unofficial, free, no API key) ───────────────────────────────
# GOOGLE_TTS_LANG=vi
# GOOGLE_TTS_SLOW=false
# ─── OpenAI TTS ───────────────────────────────────────────────────────────────
# OPENAI_API_KEY=sk-...
# OPENAI_TTS_MODEL=tts-1-hd # tts-1 (fast) | tts-1-hd (quality) | gpt-4o-mini-tts
# OPENAI_TTS_VOICE=nova # alloy | echo | fable | onyx | nova | shimmer
# ─── ElevenLabs ───────────────────────────────────────────────────────────────
# ELEVENLABS_API_KEY=sk-...
# ELEVENLABS_VOICE_ID=21m00Tcm4TlvDq8ikWAM
# ELEVENLABS_MODEL=eleven_multilingual_v2TTS Provider Comparison:
| Provider | Cost | Quality | Needs internet | Notes |
|---|---|---|---|---|
| OmniVoice | Free | Good | No | Requires running local server |
| Google TTS | Free | Fair | Yes | Unofficial API, no key needed |
| OpenAI TTS | Paid | Very good | Yes | $0.015 / 1K characters |
| ElevenLabs | Freemium | Excellent | Yes | Most natural-sounding voice — Try free ↗ |
Open Claude Code, OpenCode, or Antigravity Agents in the project directory and type a natural-language command:
From a news article URL:
Create a template video from this article: https://techcrunch.com/...
From a YouTube / TikTok URL (requires yt-dlp):
Make a 9:16 poster-style news video from this YouTube video: https://youtube.com/watch?v=...
From an existing .txt file:
Create a news video using the script in file content.txt
From a subtitle .srt file:
Make a template video using the subtitle file transcript.srt
The agent will automatically: fetch content → write script.json → run pipeline → return the video path.
If you write or edit script.json yourself:
npm run pipeline -- output/<slug>/script.json📄 View full example
{
"version": "1.0",
"renderer": "hyperframes",
"aspect": "9:16",
"metadata": {
"title": "Apple launches iPhone 17 with 200MP camera",
"source": {
"url": "https://...",
"domain": "techcrunch.com",
"image": null
},
"channel": "VidFrame"
},
"voice": { "provider": "omnivoice", "speed": 1.0 },
"scenes": [
{
"id": "hook",
"type": "hook",
"voiceText": "Apple just announced the iPhone seventeen with a two-hundred megapixel camera.",
"templateId": "frame-liquid-bg-hero",
"inputs": {
"kicker": "🔥 Breaking",
"headline": "iPhone 17",
"subheadline": "200MP Camera",
"cta": "Follow now",
"brand": "VidFrame"
}
},
{
"id": "body-1",
"type": "body",
"voiceText": "The new sensor captures far more light, making night shots noticeably sharper.",
"templateId": "frame-pentagram-stat",
"inputs": {
"label": "Camera",
"headline": "200MP",
"subtitle": "Biggest sensor Apple has ever made",
"anchor": "200"
}
},
{
"id": "outro",
"type": "outro",
"voiceText": "Follow VidFrame for fresh tech news every day.",
"templateId": "frame-logo-outro",
"inputs": {
"brand_name": "VidFrame",
"tagline": "Tech news, every day",
"primary_url": "https://vidframe.vn"
}
}
]
}Hard rules: 3–12 scenes, first scene must be hook, last scene must be outro, and every templateId must exist inside templates/.
📁 View output file tree
output/<slug>-<timestamp>/
├── script.json # input (generated by skill or hand-written)
├── script.txt # joined voiceText — for CapCut auto-caption
├── voice/
│ ├── scene-hook.mp3 # TTS per scene (idempotent)
│ └── scene-*.mp3
├── voice-raw.mp3 # merged voice, no SFX yet (intermediate)
├── voice.mp3 # final audio, SFX mixed in + speed adjusted
├── clips/
│ ├── scene-hook.mp4 # rendered template clip (idempotent)
│ └── scene-hook-fit.mp4 # fitted to the scene's voice duration
├── video-silent.mp4 # concatenated clips, no audio yet (intermediate)
└── video.mp4 # 🎉 final output — 1080×1920 + voice + SFX
Safe to re-run. To redo TTS for a scene, delete
voice/scene-<id>.mp3; to re-render a scene, deleteclips/scene-<id>.mp4, then run the pipeline again — everything else is kept as-is.
Each template is a standalone HyperFrames project inside templates/ — index.html for 16:9 and
compositions/portrait.html for 9:16. You only fill in inputs; the template takes care of the look.
Full slot reference: templates/CATALOG.md.
| Template | Role | Best for |
|---|---|---|
frame-liquid-bg-hero |
hook | Opener — aurora hero with headline + CTA button |
frame-vignelli |
body | One striking number — dark charcoal + red accent |
frame-pentagram-stat |
body | One stat/benchmark — dark neon + bar chart |
frame-bold-poster |
body | Bold multi-line statement + large figure |
frame-build-minimal |
body | One big word popping in — dark/orange |
frame-creative-voltage |
body | Creative tagline — electric blue split + handwritten text |
frame-glitch-title |
body | Breaking news / tech — cyberpunk glitch RGB-split |
frame-aicoding-list |
body | List of 2–5 items (icon + severity tag) |
frame-aicoding-comparison |
body | Head-to-head comparison of two things |
frame-finance-vox |
body | Finance — stocks, gold, economy; navy/gold palette |
frame-mom-baby-vox |
body | Parenting — baby care, child health; warm pastels |
frame-logo-outro |
outro | Default end-card — glowing logo + name + tagline + URL |
frame-statement-outro |
outro | Alternate outro — red card on paper background |
Add your own template: create
templates/<id>/withindex.html,compositions/portrait.html,hyperframes.json,meta.json(+NOTICE.mdif vendored), then add one line toCATALOG.md. Use a font that supports Vietnamese characters.
SFX files live in assets/sfx/<category>/<name>.mp3. For each scene, the selector
(src/assets/sfx-selector.ts) picks a file using 3 tiers:
1. scene.sfx override → specified file, or { "name": "none" } to disable
2. semantic match → keywords in voiceText (warning→alert, record→success, launch→reveal …)
3. type default → hook→hook · body→callout · outro→outro
The SFX library is large and is not committed to the repo:
npm run sfx:download # download the SFX library
npm run sfx:filter # trim / filterThe pipeline renders fine without assets/sfx/ — the video just won't have sound effects.
| Layer | Technology |
|---|---|
| Runtime | Node ≥22 · TypeScript 6 · ESM · tsx |
| Render | HyperFrames 0.6.94 (HTML→MP4 via Chromium) |
| TTS | OmniVoice (local) · Google TTS · OpenAI TTS · ElevenLabs |
| TTS Speed | FFmpeg atempo — pitch-preserving speed control via TTS_SPEED |
| Schema | Zod ^4 |
| HTTP | axios + nock |
| Concurrency | p-limit |
| A/V | FFmpeg + ffprobe |
| Transcript | yt-dlp (subtitles/audio from YouTube, TikTok, Facebook) |
| Tests | Vitest ^4 |
| Orchestration | Claude Code / OpenCode / Antigravity Agents skill |
- HyperFrames — the HTML-to-video engine powering every template
- OmniVoice — local Vietnamese text-to-speech
- html-video — the HTML-to-video concept this project is inspired by
- Auto-Create-Video — the original project this builds upon
If this project saved you time, buy the author a coffee ☕️!
Click the link below to get your code now 👇
👉 https://trackecom.asia/GqBsVT8z 👈
ElevenLabs is the highest-quality TTS provider supported by VidFrame. Get started for free!
# VidFrame