@vibeo/cli 0.3.4 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,367 @@
1
+ # Vibeo Rendering (`@vibeo/renderer` + `@vibeo/cli`)
2
+
3
+ ## Overview
4
+
5
+ `@vibeo/renderer` provides the headless rendering pipeline: bundling a Vibeo project, launching Playwright browser instances, capturing frames, and stitching them into video via FFmpeg. `@vibeo/cli` wraps this in a CLI.
6
+
7
+ **When to use**: When you need to render a composition to a video file, preview it, or list available compositions.
8
+
9
+ ---
10
+
11
+ ## CLI Commands
12
+
13
+ ### `vibeo render`
14
+
15
+ Render a composition to a video file.
16
+
17
+ ```bash
18
+ bunx vibeo render --entry src/index.tsx --composition MyComp --output out.mp4
19
+ ```
20
+
21
+ **Required flags**:
22
+ | Flag | Description |
23
+ |------|-------------|
24
+ | `--entry <path>` | Path to the root file with compositions |
25
+ | `--composition <id>` | Composition ID to render |
26
+
27
+ **Optional flags**:
28
+ | Flag | Default | Description |
29
+ |------|---------|-------------|
30
+ | `--output <path>` | `out/<compositionId>.<ext>` | Output file path |
31
+ | `--fps <number>` | Composition fps | Override frames per second |
32
+ | `--frames <range>` | Full duration | Frame range, e.g. `"0-100"` |
33
+ | `--codec <codec>` | `h264` | `h264 \| h265 \| vp9 \| prores` |
34
+ | `--concurrency <n>` | `CPU cores / 2` | Parallel browser tabs |
35
+ | `--image-format <fmt>` | `png` | `png \| jpeg` |
36
+ | `--quality <n>` | `80` | JPEG quality / CRF value (0-100) |
37
+
38
+ ### `vibeo preview`
39
+
40
+ Start a dev server with live preview in the browser.
41
+
42
+ ```bash
43
+ bunx vibeo preview --entry src/index.tsx --port 3000
44
+ ```
45
+
46
+ **Required flags**:
47
+ | Flag | Description |
48
+ |------|-------------|
49
+ | `--entry <path>` | Path to the root file with compositions |
50
+
51
+ **Optional flags**:
52
+ | Flag | Default | Description |
53
+ |------|---------|-------------|
54
+ | `--port <number>` | `3000` | Port for the dev server |
55
+ | `--help`, `-h` | — | Show help |
56
+
57
+ ### `vibeo list`
58
+
59
+ List all registered compositions in the project. Bundles the entry, launches a headless browser, and prints a table of composition IDs with their dimensions, FPS, and duration.
60
+
61
+ ```bash
62
+ bunx vibeo list --entry src/index.tsx
63
+ ```
64
+
65
+ **Required flags**:
66
+ | Flag | Description |
67
+ |------|-------------|
68
+ | `--entry <path>` | Path to the root file with compositions |
69
+
70
+ **Optional flags**:
71
+ | Flag | Description |
72
+ |------|-------------|
73
+ | `--help`, `-h` | Show help |
74
+
75
+ ---
76
+
77
+ ## Programmatic Rendering API
78
+
79
+ ### `renderComposition(config, compositionInfo): Promise<string>`
80
+
81
+ Full render orchestration: bundle → capture frames → stitch video. Returns the path to the final output file.
82
+
83
+ ```ts
84
+ import { renderComposition } from "@vibeo/renderer";
85
+
86
+ const outputPath = await renderComposition(
87
+ {
88
+ entry: "src/index.tsx",
89
+ compositionId: "MyComp",
90
+ outputPath: "out/video.mp4",
91
+ codec: "h264",
92
+ imageFormat: "png",
93
+ quality: 80,
94
+ fps: null, // use composition fps
95
+ frameRange: null, // render all frames
96
+ concurrency: 4,
97
+ pixelFormat: "yuv420p",
98
+ onProgress: (p) => console.log(`${(p.percent * 100).toFixed(1)}%`),
99
+ },
100
+ { width: 1920, height: 1080, fps: 30, durationInFrames: 300 },
101
+ );
102
+ console.log(`Rendered to ${outputPath}`);
103
+ ```
104
+
105
+ ### `RenderConfig`
106
+
107
+ | Field | Type | Description |
108
+ |-------|------|-------------|
109
+ | `entry` | `string` | Path to the entry file |
110
+ | `compositionId` | `string` | Composition ID |
111
+ | `outputPath` | `string` | Output video path |
112
+ | `codec` | `Codec` | Video codec |
113
+ | `imageFormat` | `ImageFormat` | `"png" \| "jpeg"` |
114
+ | `quality` | `number` | 0-100 quality/CRF |
115
+ | `fps` | `number \| null` | FPS override |
116
+ | `frameRange` | `FrameRange \| null` | `[start, end]` or null for all |
117
+ | `concurrency` | `number` | Parallel browser tabs |
118
+ | `pixelFormat` | `string` | FFmpeg pixel format |
119
+ | `onProgress?` | `(progress: RenderProgress) => void` | Progress callback |
120
+
121
+ ### `RenderProgress`
122
+
123
+ | Field | Type | Description |
124
+ |-------|------|-------------|
125
+ | `framesRendered` | `number` | Frames completed |
126
+ | `totalFrames` | `number` | Total frames |
127
+ | `percent` | `number` | 0-1 fraction |
128
+ | `etaMs` | `number \| null` | Estimated time remaining (ms) |
129
+
130
+ ### Browser Lifecycle
131
+
132
+ ```ts
133
+ import { launchBrowser, closeBrowser, createPage } from "@vibeo/renderer";
134
+
135
+ const browser = await launchBrowser();
136
+ const page = await createPage(browser, 1920, 1080); // browser, width, height
137
+ // ... render frames ...
138
+ await closeBrowser(); // closes the singleton browser, no argument needed
139
+ ```
140
+
141
+ ### Bundler
142
+
143
+ ```ts
144
+ import { bundle } from "@vibeo/renderer";
145
+
146
+ const result: BundleResult = await bundle(entryPath);
147
+ // result.outDir — bundled output directory
148
+ // result.url — URL to access bundled app
149
+ // result.cleanup() — stop server and remove temp files
150
+ ```
151
+
152
+ ### Frame Operations
153
+
154
+ ```ts
155
+ import { seekToFrame, loadBundle, captureFrame } from "@vibeo/renderer";
156
+
157
+ await loadBundle(page, bundleUrl);
158
+ await seekToFrame(page, 42, "MyComp");
159
+ const filePath = await captureFrame(page, outputDir, frameNumber, {
160
+ imageFormat: "png",
161
+ quality: 80, // optional, for jpeg
162
+ });
163
+ ```
164
+
165
+ ### Frame Range Utilities
166
+
167
+ ```ts
168
+ import { parseFrameRange, getRealFrameRange, validateFrameRange } from "@vibeo/renderer";
169
+
170
+ const range = parseFrameRange("0-100", 300); // parseFrameRange(input, durationInFrames) → [0, 100]
171
+ const real = getRealFrameRange(300, range); // getRealFrameRange(durationInFrames, frameRange) → [0, 100]
172
+ const fullRange = getRealFrameRange(300, null); // → [0, 299]
173
+ validateFrameRange([0, 100], 300); // throws on invalid
174
+ ```
175
+
176
+ ### FFmpeg Stitching
177
+
178
+ ```ts
179
+ import { stitchFrames, getContainerExt, stitchAudio } from "@vibeo/renderer";
180
+
181
+ await stitchFrames({
182
+ framesDir: "/tmp/frames",
183
+ outputPath: "out.mp4",
184
+ fps: 30,
185
+ codec: "h264",
186
+ imageFormat: "png",
187
+ pixelFormat: "yuv420p",
188
+ quality: 80,
189
+ width: 1920,
190
+ height: 1080,
191
+ });
192
+
193
+ const ext = getContainerExt("vp9"); // "webm"
194
+
195
+ await stitchAudio({
196
+ videoPath: "out.mp4",
197
+ audioPaths: ["/tmp/audio.wav"],
198
+ outputPath: "final.mp4",
199
+ });
200
+ ```
201
+
202
+ ### Parallel Rendering
203
+
204
+ ```ts
205
+ import { parallelRender } from "@vibeo/renderer";
206
+
207
+ await parallelRender({
208
+ browser, // Playwright Browser instance
209
+ bundleUrl, // URL from bundle()
210
+ compositionId: "MyComp",
211
+ frameRange: [0, 299], // [start, end] inclusive
212
+ outputDir: "/tmp/frames",
213
+ width: 1920,
214
+ height: 1080,
215
+ concurrency: 8,
216
+ imageFormat: "png",
217
+ quality: 80, // optional
218
+ onProgress: (p) => {}, // optional RenderProgress callback
219
+ });
220
+ ```
221
+
222
+ ---
223
+
224
+ ## Codec Options
225
+
226
+ | Codec | Container | Use Case |
227
+ |-------|-----------|----------|
228
+ | `h264` | `.mp4` | General purpose, best compatibility |
229
+ | `h265` | `.mp4` | Better compression, less compatible |
230
+ | `vp9` | `.webm` | Web delivery, open format |
231
+ | `prores` | `.mov` | Professional editing, lossless quality |
232
+
233
+ ### When to use each
234
+
235
+ - **`h264`**: Default. Works everywhere. Good quality/size trade-off.
236
+ - **`h265`**: 30-50% smaller files than h264 at same quality. Use when file size matters and playback devices support it.
237
+ - **`vp9`**: Best for web embedding. Royalty-free.
238
+ - **`prores`**: Editing workflows (Premiere, DaVinci). Large files but no quality loss.
239
+
240
+ ---
241
+
242
+ ## Output Format Options
243
+
244
+ - **`png` frames**: Lossless intermediate frames. Larger but no quality loss during capture.
245
+ - **`jpeg` frames**: Lossy but much smaller intermediate files. Use `--quality` to control (80 is good default).
246
+ - **`pixelFormat: "yuv420p"`**: Standard for h264/h265. Use `"yuv444p"` for maximum color fidelity with prores.
247
+
248
+ ---
249
+
250
+ ## Parallel Rendering Config
251
+
252
+ ```bash
253
+ # Use 8 parallel browser tabs
254
+ bunx vibeo render --entry src/index.tsx --composition MyComp --concurrency 8
255
+ ```
256
+
257
+ The frame range is split evenly across tabs. Default concurrency is `CPU cores / 2`.
258
+
259
+ For a 300-frame video with `--concurrency 4`:
260
+ - Tab 1: frames 0-74
261
+ - Tab 2: frames 75-149
262
+ - Tab 3: frames 150-224
263
+ - Tab 4: frames 225-299
264
+
265
+ ---
266
+
267
+ ## Types
268
+
269
+ ```ts
270
+ import type {
271
+ RenderConfig,
272
+ RenderProgress,
273
+ Codec,
274
+ ImageFormat,
275
+ FrameRange,
276
+ StitchOptions,
277
+ AudioMuxOptions,
278
+ BundleResult,
279
+ } from "@vibeo/renderer";
280
+ ```
281
+
282
+ ---
283
+
284
+ ## Gotchas and Tips
285
+
286
+ 1. **FFmpeg must be installed** and available on `PATH` for stitching to work.
287
+
288
+ 2. **Playwright is required** — the renderer uses it for headless browser rendering. Install with `bunx playwright install chromium`.
289
+
290
+ 3. **Frame capture is the bottleneck** — increase `--concurrency` on machines with many cores to speed up rendering.
291
+
292
+ 4. **`--frames` range is inclusive** — `--frames=0-100` renders 101 frames.
293
+
294
+ 5. **Output directory is created automatically** — you don't need to `mkdir` before rendering.
295
+
296
+ 6. **`pixelFormat: "yuv420p"`** is required for most players to handle h264/h265 correctly.
297
+
298
+
299
+ ---
300
+
301
+ ## LLM & Agent Integration
302
+
303
+ Vibeo's CLI is built with [incur](https://github.com/wevm/incur), making it natively discoverable by AI agents and LLMs.
304
+
305
+ ### Discovering the API
306
+
307
+ ```bash
308
+ # Get a compact summary of all CLI commands (ideal for LLM system prompts)
309
+ bunx @vibeo/cli --llms
310
+
311
+ # Get the full manifest with schemas, examples, and argument details
312
+ bunx @vibeo/cli --llms-full
313
+
314
+ # Get JSON Schema for a specific command (useful for structured tool calls)
315
+ bunx @vibeo/cli render --schema
316
+ bunx @vibeo/cli create --schema
317
+ ```
318
+
319
+ ### Using as an MCP Server
320
+
321
+ ```bash
322
+ # Start Vibeo as an MCP (Model Context Protocol) server
323
+ bunx @vibeo/cli --mcp
324
+
325
+ # Register as a persistent MCP server for your agent
326
+ bunx @vibeo/cli mcp add
327
+ ```
328
+
329
+ This lets LLMs call `create`, `render`, `preview`, and `list` as structured tool calls through the MCP protocol.
330
+
331
+ ### Generating Skill Files
332
+
333
+ ```bash
334
+ # Sync skill files to your agent's skill directory
335
+ bunx @vibeo/cli skills add
336
+ ```
337
+
338
+ This generates markdown skill files that agents like Claude Code can discover and use to write Vibeo code without reading source.
339
+
340
+ ### Agent-Friendly Output
341
+
342
+ ```bash
343
+ # Output as JSON for programmatic consumption
344
+ bunx @vibeo/cli list --entry src/index.tsx --format json
345
+
346
+ # Output as YAML
347
+ bunx @vibeo/cli list --entry src/index.tsx --format yaml
348
+
349
+ # Filter output to specific keys
350
+ bunx @vibeo/cli list --entry src/index.tsx --filter-output compositions[0].id
351
+
352
+ # Count tokens in output (useful for context window planning)
353
+ bunx @vibeo/cli render --schema --token-count
354
+ ```
355
+
356
+ ### How LLMs Should Use Vibeo
357
+
358
+ 1. **Create a project**: `bunx @vibeo/cli create my-video --template basic`
359
+ 2. **Install deps**: `cd my-video && bun install`
360
+ 3. **Install Playwright** (REQUIRED for render/list): `bunx playwright install chromium`
361
+ 4. **Edit `src/index.tsx`**: Write React components using `@vibeo/core` hooks and components
362
+ 5. **Preview**: `bunx @vibeo/cli preview --entry src/index.tsx`
363
+ 6. **Render**: `bunx @vibeo/cli render --entry src/index.tsx --composition MyComp`
364
+
365
+ **Step 3 is mandatory** — `vibeo render` and `vibeo list` will fail with a cryptic Playwright error without it. `vibeo preview` works without it (uses Bun.serve, not Playwright).
366
+
367
+ All commands accept `--format json` for structured output that LLMs can parse reliably.
@@ -0,0 +1,319 @@
1
+ # Vibeo TikTok / Short-Form Video Generator
2
+
3
+ Generate TikTok, YouTube Shorts, Instagram Reels, and other vertical (9:16) videos using Vibeo.
4
+
5
+ **Trigger**: user says "TikTok", "Short", "Reel", "vertical video", "short-form", or asks for content for social media platforms.
6
+
7
+ ---
8
+
9
+ ## Format: ALWAYS 1080x1920 (9:16)
10
+
11
+ ```tsx
12
+ <Composition
13
+ id="MyTikTok"
14
+ component={TikTokVideo}
15
+ width={1080}
16
+ height={1920}
17
+ fps={30}
18
+ durationInFrames={450} // 15 seconds
19
+ />
20
+ ```
21
+
22
+ ### Duration Guidelines
23
+
24
+ | Platform | Sweet Spot | Max | Frames @ 30fps |
25
+ |----------|-----------|-----|-----------------|
26
+ | **TikTok** | 15-60s | 10 min | 450-1800 |
27
+ | **YouTube Short** | 30-60s | 3 min | 900-1800 |
28
+ | **Instagram Reel** | 15-30s | 20 min | 450-900 |
29
+ | **Twitter/X** | 15-45s | 2m20s | 450-1350 |
30
+
31
+ ---
32
+
33
+ ## Vertical Layout Rules
34
+
35
+ ### DO
36
+
37
+ ```tsx
38
+ // Stack vertically — top to bottom flow
39
+ <div style={{
40
+ width: 1080, height: 1920,
41
+ display: "flex", flexDirection: "column",
42
+ justifyContent: "center", alignItems: "center",
43
+ padding: "100px 60px 150px", // safe zones: top status bar + bottom nav
44
+ }}>
45
+ <h1 style={{ fontSize: 72, textAlign: "center" }}>Title</h1>
46
+ <div style={{ marginTop: 40 }}>
47
+ {/* Content */}
48
+ </div>
49
+ </div>
50
+ ```
51
+
52
+ ### DON'T
53
+
54
+ - No side-by-side layouts (too narrow at 1080px)
55
+ - No font size < 36px (unreadable on phones)
56
+ - No content in top 100px (status bar) or bottom 150px (nav gestures)
57
+ - No multi-column grids
58
+ - No landscape aspect ratios embedded inside
59
+
60
+ ---
61
+
62
+ ## Complete TikTok Template
63
+
64
+ Hook → Content → CTA pattern (the standard viral format):
65
+
66
+ ```tsx
67
+ import React from "react";
68
+ import {
69
+ Composition, Sequence, VibeoRoot,
70
+ useCurrentFrame, useVideoConfig, interpolate, easeOut, easeInOut,
71
+ } from "@vibeo/core";
72
+
73
+ // ---- Scene Timing (centralized) ----
74
+ const SCENES = {
75
+ hook: { from: 0, duration: 90 }, // 3s — attention grabber
76
+ content: { from: 90, duration: 270 }, // 9s — main content
77
+ cta: { from: 360, duration: 90 }, // 3s — call to action
78
+ } as const;
79
+ const TOTAL = 450; // 15s
80
+
81
+ // ---- Hook Scene (first 3 seconds = make or break) ----
82
+ function HookScene({ text }: { text: string }) {
83
+ const frame = useCurrentFrame();
84
+ const { width, height } = useVideoConfig();
85
+
86
+ const scale = interpolate(frame, [0, 15], [0.5, 1], {
87
+ easing: easeOut, extrapolateRight: "clamp",
88
+ });
89
+ const opacity = interpolate(frame, [0, 10], [0, 1], { extrapolateRight: "clamp" });
90
+
91
+ // Pulsing glow effect
92
+ const glow = interpolate(frame % 30, [0, 15, 30], [0, 15, 0]);
93
+
94
+ return (
95
+ <div style={{
96
+ width, height, display: "flex", justifyContent: "center", alignItems: "center",
97
+ background: "linear-gradient(180deg, #0a0a0a, #1a0a2e)",
98
+ padding: "100px 60px 150px",
99
+ }}>
100
+ <h1 style={{
101
+ fontSize: 80, fontWeight: 900, color: "white",
102
+ textAlign: "center", lineHeight: 1.2,
103
+ opacity, transform: `scale(${scale})`,
104
+ textShadow: `0 0 ${glow}px rgba(138, 92, 246, 0.8)`,
105
+ }}>
106
+ {text}
107
+ </h1>
108
+ </div>
109
+ );
110
+ }
111
+
112
+ // ---- Content Scene (staggered points) ----
113
+ function ContentScene({ points }: { points: string[] }) {
114
+ const frame = useCurrentFrame();
115
+ const { width, height } = useVideoConfig();
116
+
117
+ return (
118
+ <div style={{
119
+ width, height, display: "flex", flexDirection: "column",
120
+ justifyContent: "center", padding: "100px 60px 150px",
121
+ background: "#0a0a0a", gap: 24,
122
+ }}>
123
+ {points.map((point, i) => {
124
+ const delay = 15 + i * 20;
125
+ const opacity = interpolate(frame, [delay, delay + 15], [0, 1], {
126
+ extrapolateLeft: "clamp", extrapolateRight: "clamp",
127
+ });
128
+ const x = interpolate(frame, [delay, delay + 15], [-40, 0], {
129
+ easing: easeOut, extrapolateLeft: "clamp", extrapolateRight: "clamp",
130
+ });
131
+
132
+ return (
133
+ <div key={i} style={{
134
+ opacity, transform: `translateX(${x}px)`,
135
+ display: "flex", alignItems: "center", gap: 16,
136
+ }}>
137
+ <div style={{
138
+ width: 48, height: 48, borderRadius: 12,
139
+ background: "linear-gradient(135deg, #8b5cf6, #06b6d4)",
140
+ display: "flex", alignItems: "center", justifyContent: "center",
141
+ fontSize: 24, fontWeight: 700, color: "white",
142
+ }}>
143
+ {i + 1}
144
+ </div>
145
+ <span style={{ fontSize: 40, color: "white", fontWeight: 600 }}>
146
+ {point}
147
+ </span>
148
+ </div>
149
+ );
150
+ })}
151
+ </div>
152
+ );
153
+ }
154
+
155
+ // ---- CTA Scene ----
156
+ function CTAScene({ text, subtext }: { text: string; subtext: string }) {
157
+ const frame = useCurrentFrame();
158
+ const { width, height } = useVideoConfig();
159
+
160
+ const scale = interpolate(frame, [0, 20], [0.8, 1], {
161
+ easing: easeOut, extrapolateRight: "clamp",
162
+ });
163
+ // Pulsing button effect
164
+ const btnScale = interpolate(frame % 30, [0, 15, 30], [1, 1.05, 1]);
165
+
166
+ return (
167
+ <div style={{
168
+ width, height, display: "flex", flexDirection: "column",
169
+ justifyContent: "center", alignItems: "center",
170
+ background: "linear-gradient(180deg, #0a0a0a, #1a0a2e)",
171
+ padding: "100px 60px 150px", transform: `scale(${scale})`,
172
+ }}>
173
+ <h1 style={{ fontSize: 72, fontWeight: 900, color: "white", textAlign: "center" }}>
174
+ {text}
175
+ </h1>
176
+ <div style={{
177
+ marginTop: 40, padding: "20px 60px", borderRadius: 50,
178
+ background: "linear-gradient(135deg, #8b5cf6, #06b6d4)",
179
+ fontSize: 36, fontWeight: 700, color: "white",
180
+ transform: `scale(${btnScale})`,
181
+ }}>
182
+ {subtext}
183
+ </div>
184
+ </div>
185
+ );
186
+ }
187
+
188
+ // ---- Main Video ----
189
+ function TikTokVideo() {
190
+ return (
191
+ <>
192
+ <Sequence from={SCENES.hook.from} durationInFrames={SCENES.hook.duration}>
193
+ <HookScene text="3 things you didn't know about React" />
194
+ </Sequence>
195
+ <Sequence from={SCENES.content.from} durationInFrames={SCENES.content.duration}>
196
+ <ContentScene points={[
197
+ "Server Components are default",
198
+ "use() replaces useEffect",
199
+ "Actions replace forms",
200
+ ]} />
201
+ </Sequence>
202
+ <Sequence from={SCENES.cta.from} durationInFrames={SCENES.cta.duration}>
203
+ <CTAScene text="Follow for more" subtext="Like & Share →" />
204
+ </Sequence>
205
+ </>
206
+ );
207
+ }
208
+
209
+ // ---- Root ----
210
+ export function Root() {
211
+ return (
212
+ <VibeoRoot>
213
+ <Composition
214
+ id="TikTokVideo"
215
+ component={TikTokVideo}
216
+ width={1080}
217
+ height={1920}
218
+ fps={30}
219
+ durationInFrames={TOTAL}
220
+ />
221
+ </VibeoRoot>
222
+ );
223
+ }
224
+ ```
225
+
226
+ ---
227
+
228
+ ## TikTok Content Patterns
229
+
230
+ ### 1. "Did You Know" / Listicle (most common)
231
+
232
+ ```
233
+ Hook (3s): Bold question or surprising claim
234
+ Content (9-20s): 3-5 staggered points with numbers
235
+ CTA (3s): Follow / Like / Share
236
+ ```
237
+
238
+ ### 2. Before/After
239
+
240
+ ```
241
+ Hook (2s): "Before vs After"
242
+ Before (5s): Show the "bad" version (top half)
243
+ After (5s): Reveal the "good" version (bottom half) — use slide transition
244
+ CTA (3s): "Try it yourself"
245
+ ```
246
+
247
+ For vertical before/after, stack top/bottom:
248
+ ```tsx
249
+ <div style={{ display: "flex", flexDirection: "column", width: 1080, height: 1920 }}>
250
+ <div style={{ flex: 1 }}>{before}</div>
251
+ <div style={{ height: 4, background: "#333" }} />
252
+ <div style={{ flex: 1 }}>{after}</div>
253
+ </div>
254
+ ```
255
+
256
+ ### 3. Code Tutorial
257
+
258
+ ```
259
+ Hook (3s): "This one trick..."
260
+ Code (10-15s): Animated code block, line by line reveal
261
+ Result (5s): Show the output/demo
262
+ CTA (3s): "Link in bio"
263
+ ```
264
+
265
+ Use the CodeBlock recipe from vibeo-effects skill, but with larger font (28px) for vertical.
266
+
267
+ ### 4. Product/Feature Showcase
268
+
269
+ ```
270
+ Hook (3s): Pain point question
271
+ Demo (10s): Screen recording or animated mockup
272
+ Features (5s): 3 key bullets, staggered
273
+ CTA (3s): "Download now"
274
+ ```
275
+
276
+ ---
277
+
278
+ ## Animation Rules for Short-Form
279
+
280
+ 1. **First 3 seconds are everything** — the hook must grab attention immediately. Use scale-in, glow, or spring entrance.
281
+
282
+ 2. **Constant motion** — never have a static frame for more than 1 second. Add subtle pulse, breathing, or background movement.
283
+
284
+ 3. **Text on screen** — most viewers watch without sound. Every key point should have on-screen text.
285
+
286
+ 4. **Fast transitions** — 10-15 frame transitions (0.3-0.5s). No slow fades.
287
+
288
+ 5. **Pulsing/breathing effects** — use `frame % N` for constant subtle animation:
289
+ ```tsx
290
+ const pulse = interpolate(frame % 60, [0, 30, 60], [1, 1.03, 1]);
291
+ ```
292
+
293
+ 6. **Number badges** — always number your points (1, 2, 3). It signals structure and keeps viewers watching.
294
+
295
+ 7. **Bold gradients** — dark backgrounds with vibrant gradient accents. Avoid flat solid colors.
296
+
297
+ ---
298
+
299
+ ## Rendering for Platforms
300
+
301
+ ```bash
302
+ # TikTok / Reels (H.264, most compatible)
303
+ bunx @vibeo/cli render --entry src/index.tsx --composition TikTokVideo --codec h264
304
+
305
+ # High quality (H.265, smaller file)
306
+ bunx @vibeo/cli render --entry src/index.tsx --composition TikTokVideo --codec h265
307
+
308
+ # Twitter/X (needs smaller file, use lower quality)
309
+ bunx @vibeo/cli render --entry src/index.tsx --composition TikTokVideo --codec h264 --quality 60
310
+ ```
311
+
312
+ ### Platform upload limits
313
+
314
+ | Platform | Max File Size | Recommended Codec |
315
+ |----------|--------------|-------------------|
316
+ | TikTok | 287 MB | H.264 |
317
+ | YouTube Shorts | 256 MB | H.264 |
318
+ | Instagram Reels | 650 MB | H.264 |
319
+ | Twitter/X | 512 MB | H.264 |
@@ -246,6 +246,7 @@ export async function createProject(
246
246
  dev: "bun vibeo preview --entry src/index.tsx",
247
247
  build: "bun vibeo render --entry src/index.tsx",
248
248
  list: "bun vibeo list --entry src/index.tsx",
249
+ editor: "bun vibeo editor --entry src/index.tsx",
249
250
  typecheck: "bunx tsc --noEmit",
250
251
  },
251
252
  dependencies: {