@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.
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +1 -0
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/editor.d.ts +2 -0
- package/dist/commands/editor.d.ts.map +1 -0
- package/dist/commands/editor.js +18 -0
- package/dist/commands/editor.js.map +1 -0
- package/dist/commands/install-skills.d.ts.map +1 -1
- package/dist/commands/install-skills.js +13 -76
- package/dist/commands/install-skills.js.map +1 -1
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/skills/vibeo-audio/SKILL.md +283 -0
- package/skills/vibeo-core/SKILL.md +442 -0
- package/skills/vibeo-editor/SKILL.md +348 -0
- package/skills/vibeo-effects/SKILL.md +580 -0
- package/skills/vibeo-extras/SKILL.md +457 -0
- package/skills/vibeo-rendering/SKILL.md +367 -0
- package/skills/vibeo-tiktok/SKILL.md +319 -0
- package/src/commands/create.ts +1 -0
- package/src/commands/editor.ts +23 -0
- package/src/commands/install-skills.ts +16 -75
- package/src/index.ts +15 -0
|
@@ -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 |
|
package/src/commands/create.ts
CHANGED
|
@@ -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: {
|