@conceptcraft/mindframes 0.1.6 → 0.1.7
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/index.js +1463 -1458
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1025,7 +1025,7 @@ async function pollForCompletion(checkFn, maxAttempts = 60, intervalMs = 2e3) {
|
|
|
1025
1025
|
if (result.status === "completed" || result.status === "failed") {
|
|
1026
1026
|
return result;
|
|
1027
1027
|
}
|
|
1028
|
-
await new Promise((
|
|
1028
|
+
await new Promise((resolve6) => setTimeout(resolve6, intervalMs));
|
|
1029
1029
|
}
|
|
1030
1030
|
throw new ApiError("Operation timed out", 408, 1);
|
|
1031
1031
|
}
|
|
@@ -1132,10 +1132,10 @@ function generateState() {
|
|
|
1132
1132
|
async function findAvailablePort(start, end) {
|
|
1133
1133
|
for (let port = start; port <= end; port++) {
|
|
1134
1134
|
try {
|
|
1135
|
-
await new Promise((
|
|
1135
|
+
await new Promise((resolve6, reject) => {
|
|
1136
1136
|
const server = http.createServer();
|
|
1137
1137
|
server.listen(port, () => {
|
|
1138
|
-
server.close(() =>
|
|
1138
|
+
server.close(() => resolve6());
|
|
1139
1139
|
});
|
|
1140
1140
|
server.on("error", reject);
|
|
1141
1141
|
});
|
|
@@ -1193,7 +1193,7 @@ async function exchangeCodeForTokens(tokenEndpoint, code, codeVerifier, redirect
|
|
|
1193
1193
|
return response.json();
|
|
1194
1194
|
}
|
|
1195
1195
|
function startCallbackServer(port, expectedState) {
|
|
1196
|
-
return new Promise((
|
|
1196
|
+
return new Promise((resolve6, reject) => {
|
|
1197
1197
|
let timeoutId;
|
|
1198
1198
|
let settled = false;
|
|
1199
1199
|
const cleanup = () => {
|
|
@@ -1279,7 +1279,7 @@ function startCallbackServer(port, expectedState) {
|
|
|
1279
1279
|
</html>
|
|
1280
1280
|
`);
|
|
1281
1281
|
cleanup();
|
|
1282
|
-
|
|
1282
|
+
resolve6({ code, state });
|
|
1283
1283
|
});
|
|
1284
1284
|
server.listen(port);
|
|
1285
1285
|
process.once("SIGINT", onCancel);
|
|
@@ -2422,21 +2422,21 @@ Uploading ${options.file.length} file(s)...`));
|
|
|
2422
2422
|
}
|
|
2423
2423
|
});
|
|
2424
2424
|
async function readStdin() {
|
|
2425
|
-
return new Promise((
|
|
2425
|
+
return new Promise((resolve6) => {
|
|
2426
2426
|
let data = "";
|
|
2427
2427
|
process.stdin.setEncoding("utf8");
|
|
2428
2428
|
if (process.stdin.isTTY) {
|
|
2429
|
-
|
|
2429
|
+
resolve6("");
|
|
2430
2430
|
return;
|
|
2431
2431
|
}
|
|
2432
2432
|
process.stdin.on("data", (chunk) => {
|
|
2433
2433
|
data += chunk;
|
|
2434
2434
|
});
|
|
2435
2435
|
process.stdin.on("end", () => {
|
|
2436
|
-
|
|
2436
|
+
resolve6(data.trim());
|
|
2437
2437
|
});
|
|
2438
2438
|
setTimeout(() => {
|
|
2439
|
-
|
|
2439
|
+
resolve6(data.trim());
|
|
2440
2440
|
}, 100);
|
|
2441
2441
|
});
|
|
2442
2442
|
}
|
|
@@ -3127,170 +3127,197 @@ var whoamiCommand = new Command13("whoami").description("Show current user and t
|
|
|
3127
3127
|
}
|
|
3128
3128
|
});
|
|
3129
3129
|
|
|
3130
|
-
// src/commands/skill.ts
|
|
3130
|
+
// src/commands/skill/index.ts
|
|
3131
3131
|
init_output();
|
|
3132
3132
|
import { Command as Command14 } from "commander";
|
|
3133
3133
|
import chalk12 from "chalk";
|
|
3134
|
-
import { mkdirSync, writeFileSync, existsSync as existsSync2 } from "fs";
|
|
3135
|
-
import { join } from "path";
|
|
3136
|
-
import { homedir } from "os";
|
|
3137
|
-
import { execSync } from "child_process";
|
|
3138
|
-
function generateSkillContent(b) {
|
|
3139
|
-
const cmd2 = b.name;
|
|
3140
|
-
const pkg = b.packageName;
|
|
3141
|
-
const url = b.apiUrl;
|
|
3142
|
-
const name = b.displayName;
|
|
3143
|
-
return `---
|
|
3144
|
-
name: ${cmd2}
|
|
3145
|
-
description: Create AI-powered presentations and videos. Use for slides, decks, video content, voiceovers, and music generation.
|
|
3146
|
-
metadata:
|
|
3147
|
-
tags: presentations, video, tts, music, animation, remotion
|
|
3148
|
-
video:
|
|
3149
|
-
description: End-to-end AI video creation workflow with TTS voiceover and music generation. Use this skill when users want to create videos, promotional content, explainers, tourism videos, product demos, or any video content from an idea or topic. Handles the complete workflow - research, script writing, asset gathering, audio generation (voiceover + music), and orchestrates video creation. Use together with remotion-best-practices skill for Remotion-specific patterns. Triggers on requests like "create a video about X", "make a promotional video", "build a video for Y", or any video content creation task.
|
|
3150
|
-
---
|
|
3151
3134
|
|
|
3152
|
-
|
|
3135
|
+
// src/commands/skill/sections/frontmatter.ts
|
|
3136
|
+
var frontmatter = {
|
|
3137
|
+
title: "Frontmatter",
|
|
3138
|
+
render: (ctx) => `---
|
|
3139
|
+
name: ${ctx.cmd}
|
|
3140
|
+
description: Use when user asks to create presentations (slides, decks, pitch decks) or videos (product demos, explainers, social content, promos). Also handles voiceover and music generation.
|
|
3141
|
+
---`
|
|
3142
|
+
};
|
|
3153
3143
|
|
|
3154
|
-
|
|
3144
|
+
// src/commands/skill/sections/header.ts
|
|
3145
|
+
var header3 = {
|
|
3146
|
+
title: "Header",
|
|
3147
|
+
render: (ctx) => `# ${ctx.name} CLI
|
|
3155
3148
|
|
|
3156
|
-
|
|
3149
|
+
Create professional presentations directly from your terminal. The CLI generates AI-powered slides from any context you provide - text, files, URLs, or piped content.`
|
|
3150
|
+
};
|
|
3151
|
+
|
|
3152
|
+
// src/commands/skill/sections/prerequisites.ts
|
|
3153
|
+
var prerequisites = {
|
|
3154
|
+
title: "Prerequisites",
|
|
3155
|
+
render: (ctx) => `## Prerequisites
|
|
3157
3156
|
|
|
3158
3157
|
\`\`\`bash
|
|
3159
|
-
npm install -g ${pkg}
|
|
3160
|
-
${
|
|
3161
|
-
${
|
|
3158
|
+
npm install -g ${ctx.pkg}
|
|
3159
|
+
${ctx.cmd} login # Authenticate (opens browser)
|
|
3160
|
+
${ctx.cmd} whoami # Verify auth
|
|
3162
3161
|
\`\`\`
|
|
3163
3162
|
|
|
3164
|
-
|
|
3163
|
+
### Authentication
|
|
3165
3164
|
|
|
3166
|
-
|
|
3165
|
+
If not authenticated, run \`${ctx.cmd} login\` - it opens browser for OAuth.
|
|
3166
|
+
If login fails or user declines, fall back to API key: \`${ctx.cmd} config init\``
|
|
3167
|
+
};
|
|
3167
3168
|
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
- **remotion-best-practices** skill (auto-installed) - Remotion-specific patterns
|
|
3169
|
+
// src/commands/skill/sections/workflow.ts
|
|
3170
|
+
var workflow = {
|
|
3171
|
+
title: "Core Workflow",
|
|
3172
|
+
render: (ctx) => `## Core Workflow
|
|
3173
3173
|
|
|
3174
|
-
|
|
3174
|
+
1. **Gather context** - Read relevant files, code, or documentation
|
|
3175
|
+
2. **Create presentation** - Pass context to \`${ctx.cmd} create\`
|
|
3176
|
+
3. **Share URL** - Return the presentation link to the user`
|
|
3177
|
+
};
|
|
3175
3178
|
|
|
3176
|
-
|
|
3179
|
+
// src/commands/skill/sections/create-command.ts
|
|
3180
|
+
var createCommand2 = {
|
|
3181
|
+
title: "Create Command",
|
|
3182
|
+
render: (ctx) => `## Commands
|
|
3177
3183
|
|
|
3178
|
-
|
|
3179
|
-
# Create from context
|
|
3180
|
-
cat README.md | ${cmd2} create "Project Overview"
|
|
3184
|
+
### Create Presentation
|
|
3181
3185
|
|
|
3182
|
-
|
|
3183
|
-
${cmd2} create "Product Demo" --file ./deck.pptx --file ./logo.png
|
|
3186
|
+
Context is **required**. Provide it via one of these methods:
|
|
3184
3187
|
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
+
\`\`\`bash
|
|
3189
|
+
# Upload files (PDFs, PPTX, images, docs)
|
|
3190
|
+
${ctx.cmd} create "Product Overview" --file ./deck.pptx --file ./logo.png
|
|
3188
3191
|
|
|
3189
|
-
|
|
3192
|
+
# Direct text context
|
|
3193
|
+
${ctx.cmd} create "Topic Title" --context "Key points, data, facts..."
|
|
3190
3194
|
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
${cmd2} video init my-video
|
|
3195
|
+
# From a text file
|
|
3196
|
+
${ctx.cmd} create "Topic Title" --context-file ./notes.md
|
|
3194
3197
|
|
|
3195
|
-
#
|
|
3196
|
-
${
|
|
3198
|
+
# Pipe content (auto-detected)
|
|
3199
|
+
cat README.md | ${ctx.cmd} create "Project Overview"
|
|
3197
3200
|
|
|
3198
|
-
#
|
|
3199
|
-
${
|
|
3200
|
-
${cmd2} video search "tech workspace" -n 5
|
|
3201
|
-
\`\`\`
|
|
3201
|
+
# From URLs (scraped automatically)
|
|
3202
|
+
${ctx.cmd} create "Competitor Analysis" --sources https://example.com/report
|
|
3202
3203
|
|
|
3203
|
-
|
|
3204
|
+
# Combine multiple sources
|
|
3205
|
+
cat src/auth/*.ts | ${ctx.cmd} create "Auth System" \\
|
|
3206
|
+
--file ./architecture.png \\
|
|
3207
|
+
--context "Focus on security patterns"
|
|
3208
|
+
\`\`\``
|
|
3209
|
+
};
|
|
3204
3210
|
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3211
|
+
// src/commands/skill/sections/create-options.ts
|
|
3212
|
+
var createOptions = {
|
|
3213
|
+
title: "Create Options",
|
|
3214
|
+
render: (_ctx) => `### Create Options
|
|
3208
3215
|
|
|
3209
|
-
|
|
3210
|
-
|
|
3216
|
+
| Option | Description | Default |
|
|
3217
|
+
|--------|-------------|---------|
|
|
3218
|
+
| \`-n, --slides <count>\` | Number of slides (1-20) | 10 |
|
|
3219
|
+
| \`-m, --mode <mode>\` | Quality: \`instant\`, \`fast\`, \`balanced\`, \`best\` | balanced |
|
|
3220
|
+
| \`-t, --tone <tone>\` | Tone: \`professional\`, \`educational\`, \`creative\`, \`formal\`, \`casual\` | professional |
|
|
3221
|
+
| \`--amount <amount>\` | Density: \`minimal\`, \`concise\`, \`detailed\`, \`extensive\` | concise |
|
|
3222
|
+
| \`--audience <text>\` | Target audience | General Audience |
|
|
3223
|
+
| \`-g, --goal <type>\` | Purpose: \`inform\`, \`persuade\`, \`train\`, \`learn\`, \`entertain\`, \`report\` | - |
|
|
3224
|
+
| \`--custom-goal <text>\` | Custom goal description | - |
|
|
3225
|
+
| \`-f, --file <paths...>\` | Files to upload (PDF, PPTX, images, docs) | - |
|
|
3226
|
+
| \`-l, --language <lang>\` | Output language | en |
|
|
3227
|
+
| \`-b, --brand <id>\` | Branding ID to apply | - |
|
|
3228
|
+
| \`-o, --output <format>\` | Output: \`human\`, \`json\`, \`quiet\` | human |`
|
|
3229
|
+
};
|
|
3211
3230
|
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3231
|
+
// src/commands/skill/sections/other-commands.ts
|
|
3232
|
+
var otherCommands = {
|
|
3233
|
+
title: "Other Commands",
|
|
3234
|
+
render: (ctx) => `### Other Commands
|
|
3215
3235
|
|
|
3216
|
-
|
|
3236
|
+
\`\`\`bash
|
|
3237
|
+
# Check authentication
|
|
3238
|
+
${ctx.cmd} whoami
|
|
3217
3239
|
|
|
3218
|
-
|
|
3240
|
+
# List presentations
|
|
3241
|
+
${ctx.cmd} list
|
|
3242
|
+
${ctx.cmd} list --format json
|
|
3219
3243
|
|
|
3220
|
-
|
|
3244
|
+
# Get presentation details
|
|
3245
|
+
${ctx.cmd} get <id-or-slug>
|
|
3221
3246
|
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
}
|
|
3225
|
-
function generatePresentationsRule(b) {
|
|
3226
|
-
const cmd2 = b.name;
|
|
3227
|
-
const url = b.apiUrl;
|
|
3228
|
-
return `---
|
|
3229
|
-
name: presentations
|
|
3230
|
-
description: Creating AI-powered presentations
|
|
3231
|
-
---
|
|
3247
|
+
# Export to ZIP
|
|
3248
|
+
${ctx.cmd} export <id-or-slug> -o presentation.zip
|
|
3232
3249
|
|
|
3233
|
-
#
|
|
3250
|
+
# Import presentation
|
|
3251
|
+
${ctx.cmd} import ./presentation.zip
|
|
3234
3252
|
|
|
3235
|
-
|
|
3253
|
+
# Manage branding
|
|
3254
|
+
${ctx.cmd} branding list
|
|
3255
|
+
${ctx.cmd} branding extract https://company.com
|
|
3236
3256
|
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3257
|
+
# Install/manage this skill
|
|
3258
|
+
${ctx.cmd} skill install
|
|
3259
|
+
${ctx.cmd} skill show
|
|
3260
|
+
\`\`\``
|
|
3261
|
+
};
|
|
3240
3262
|
|
|
3241
|
-
|
|
3263
|
+
// src/commands/skill/sections/examples.ts
|
|
3264
|
+
var examples = {
|
|
3265
|
+
title: "Examples",
|
|
3266
|
+
render: (ctx) => `## Examples
|
|
3242
3267
|
|
|
3243
|
-
|
|
3268
|
+
### Present a Codebase Feature
|
|
3244
3269
|
|
|
3245
3270
|
\`\`\`bash
|
|
3246
|
-
#
|
|
3247
|
-
${
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3271
|
+
# Read the relevant files and create presentation
|
|
3272
|
+
cat src/lib/auth.ts src/lib/session.ts | ${ctx.cmd} create "Authentication System" \\
|
|
3273
|
+
--slides 8 --tone educational --audience "New developers" \\
|
|
3274
|
+
--goal train
|
|
3275
|
+
\`\`\`
|
|
3251
3276
|
|
|
3252
|
-
|
|
3253
|
-
${cmd2} create "Topic Title" --context-file ./notes.md
|
|
3277
|
+
### Technical Documentation with Diagrams
|
|
3254
3278
|
|
|
3255
|
-
|
|
3256
|
-
|
|
3279
|
+
\`\`\`bash
|
|
3280
|
+
${ctx.cmd} create "API Reference" \\
|
|
3281
|
+
--file ./docs/api.md \\
|
|
3282
|
+
--file ./diagrams/architecture.png \\
|
|
3283
|
+
--mode best --amount detailed \\
|
|
3284
|
+
--goal inform
|
|
3285
|
+
\`\`\`
|
|
3257
3286
|
|
|
3258
|
-
|
|
3259
|
-
${cmd2} create "Competitor Analysis" --sources https://example.com/report
|
|
3287
|
+
### Quick Project Overview
|
|
3260
3288
|
|
|
3261
|
-
|
|
3262
|
-
cat
|
|
3263
|
-
--
|
|
3264
|
-
--context "Focus on security patterns"
|
|
3289
|
+
\`\`\`bash
|
|
3290
|
+
cat README.md package.json | ${ctx.cmd} create "Project Introduction" \\
|
|
3291
|
+
-m instant --slides 5
|
|
3265
3292
|
\`\`\`
|
|
3266
3293
|
|
|
3267
|
-
|
|
3294
|
+
### Sales Deck from Existing Presentation
|
|
3268
3295
|
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
| \`-g, --goal <type>\` | Purpose: \`inform\`, \`persuade\`, \`train\`, \`learn\`, \`entertain\`, \`report\` | - |
|
|
3277
|
-
| \`-f, --file <paths...>\` | Files to upload | - |
|
|
3278
|
-
| \`-l, --language <lang>\` | Output language | en |
|
|
3279
|
-
| \`-b, --brand <id>\` | Branding ID | - |
|
|
3296
|
+
\`\`\`bash
|
|
3297
|
+
${ctx.cmd} create "Product Demo" \\
|
|
3298
|
+
--file ./existing-deck.pptx \\
|
|
3299
|
+
--goal persuade \\
|
|
3300
|
+
--audience "Enterprise buyers" \\
|
|
3301
|
+
--tone professional
|
|
3302
|
+
\`\`\`
|
|
3280
3303
|
|
|
3281
|
-
|
|
3304
|
+
### Research Presentation
|
|
3282
3305
|
|
|
3283
3306
|
\`\`\`bash
|
|
3284
|
-
${
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3307
|
+
${ctx.cmd} create "Market Analysis" \\
|
|
3308
|
+
--file ./research.pdf \\
|
|
3309
|
+
--sources https://report.com/industry.pdf \\
|
|
3310
|
+
--tone formal --audience "Executive team" \\
|
|
3311
|
+
--goal report
|
|
3312
|
+
\`\`\``
|
|
3313
|
+
};
|
|
3291
3314
|
|
|
3292
|
-
|
|
3315
|
+
// src/commands/skill/sections/output.ts
|
|
3316
|
+
var output = {
|
|
3317
|
+
title: "Output",
|
|
3318
|
+
render: (ctx) => `## Output
|
|
3293
3319
|
|
|
3320
|
+
Successful creation returns:
|
|
3294
3321
|
\`\`\`
|
|
3295
3322
|
\u2713 Presentation created successfully
|
|
3296
3323
|
|
|
@@ -3298,1581 +3325,1424 @@ ${cmd2} branding extract https://... # Extract branding from URL
|
|
|
3298
3325
|
Slides: 8
|
|
3299
3326
|
Generated in: 45s \xB7 12,500 tokens
|
|
3300
3327
|
|
|
3301
|
-
Open: ${url}/en/view/presentations/auth-system-v1-abc123
|
|
3328
|
+
Open: ${ctx.url}/en/view/presentations/auth-system-v1-abc123
|
|
3302
3329
|
\`\`\`
|
|
3303
|
-
`;
|
|
3304
|
-
}
|
|
3305
|
-
function generateVideoRule(b) {
|
|
3306
|
-
const cmd2 = b.name;
|
|
3307
|
-
return `---
|
|
3308
|
-
name: video
|
|
3309
|
-
description: Video creation workflow - project-based UI replication AND stock-based videos
|
|
3310
|
-
---
|
|
3311
|
-
|
|
3312
|
-
# Video Creation
|
|
3313
3330
|
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3331
|
+
For scripting, use JSON output:
|
|
3332
|
+
\`\`\`bash
|
|
3333
|
+
URL=$(${ctx.cmd} create "Demo" --context "..." -o json | jq -r '.viewUrl')
|
|
3334
|
+
\`\`\``
|
|
3335
|
+
};
|
|
3317
3336
|
|
|
3318
|
-
|
|
3337
|
+
// src/commands/skill/sections/best-practices.ts
|
|
3338
|
+
var bestPractices = {
|
|
3339
|
+
title: "Best Practices",
|
|
3340
|
+
render: (_ctx) => `## Best Practices
|
|
3341
|
+
|
|
3342
|
+
1. **Provide rich context** - More context = better slides. Include code, docs, data.
|
|
3343
|
+
2. **Use file uploads for binary content** - PDFs, images, PPTX files need \`--file\`.
|
|
3344
|
+
3. **Specify a goal** - Helps tailor the presentation structure and messaging.
|
|
3345
|
+
4. **Use appropriate mode** - \`instant\` for quick drafts, \`best\` for important presentations.
|
|
3346
|
+
5. **Specify audience** - Helps tailor complexity and terminology.
|
|
3347
|
+
6. **Combine sources** - Pipe multiple files for comprehensive presentations.`
|
|
3348
|
+
};
|
|
3319
3349
|
|
|
3320
|
-
|
|
3350
|
+
// src/commands/skill/sections/file-types.ts
|
|
3351
|
+
var fileTypes = {
|
|
3352
|
+
title: "Supported File Types",
|
|
3353
|
+
render: (_ctx) => `## Supported File Types
|
|
3321
3354
|
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
5. **ALWAYS FRESH PROJECT** - Delete existing video project, create new with \`${cmd2} video init\`.
|
|
3355
|
+
- **Documents**: PDF, DOCX, XLSX, PPTX
|
|
3356
|
+
- **Images**: JPEG, PNG, GIF, WebP
|
|
3357
|
+
- **Text**: Markdown, TXT, CSV, JSON`
|
|
3358
|
+
};
|
|
3327
3359
|
|
|
3328
|
-
|
|
3360
|
+
// src/commands/skill/sections/troubleshooting.ts
|
|
3361
|
+
var troubleshooting = {
|
|
3362
|
+
title: "Troubleshooting",
|
|
3363
|
+
render: (ctx) => `## Troubleshooting
|
|
3329
3364
|
|
|
3330
|
-
|
|
3365
|
+
\`\`\`bash
|
|
3366
|
+
# Check if authenticated
|
|
3367
|
+
${ctx.cmd} whoami
|
|
3331
3368
|
|
|
3332
|
-
|
|
3369
|
+
# Re-authenticate if needed
|
|
3370
|
+
${ctx.cmd} login
|
|
3333
3371
|
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
5. Skill: remotion-best-practices
|
|
3372
|
+
# Debug mode
|
|
3373
|
+
${ctx.cmd} create "Test" --context "test" --debug
|
|
3374
|
+
\`\`\``
|
|
3375
|
+
};
|
|
3339
3376
|
|
|
3340
|
-
|
|
3377
|
+
// src/commands/skill/sections/video-creation.ts
|
|
3378
|
+
var videoCreation = {
|
|
3379
|
+
title: "Video Creation",
|
|
3380
|
+
render: (ctx) => `## Video Creation
|
|
3341
3381
|
|
|
3342
|
-
|
|
3382
|
+
### How to Create a Perfect Video
|
|
3343
3383
|
|
|
3344
|
-
|
|
3345
|
-
Use when user has a project/app and wants to showcase it.
|
|
3346
|
-
- **Triggers:** "create video for my app", "product demo", "feature walkthrough", "promotional video for [project]"
|
|
3347
|
-
- **Approach:** Read components \u2192 replicate UI pixel-perfect \u2192 add animations
|
|
3348
|
-
- **Result:** Video looks IDENTICAL to the real app
|
|
3384
|
+
Videos must feel like premium tech launches (Stripe, Apple, Linear) - **Kinetic Composition**, not slideshows.
|
|
3349
3385
|
|
|
3350
|
-
|
|
3351
|
-
Use ONLY when user has NO project or explicitly wants stock content.
|
|
3352
|
-
- **Triggers:** "create a video about tourism", "make a generic explainer"
|
|
3353
|
-
- **Approach:** Use \`${cmd2} video create\` with stock images
|
|
3354
|
-
- **Result:** Generic video with stock imagery
|
|
3386
|
+
**Core Philosophy:** "Nothing sits still. Everything is physics-based. Every pixel breathes."
|
|
3355
3387
|
|
|
3356
|
-
|
|
3388
|
+
### Required Reading (The Motion Bible)
|
|
3357
3389
|
|
|
3358
|
-
|
|
3390
|
+
| Rule | Why This Matters |
|
|
3391
|
+
|------|------------------|
|
|
3392
|
+
| [video-creation-guide.md](rules/video/video-creation-guide.md) | **The Motion Bible.** Camera rigs, 2.5D entrances, cursor physics, typography, backgrounds |
|
|
3393
|
+
| [failures.md](rules/video/failures.md) | **Avoid rejection.** 7 common mistakes that make videos look like slideshows |
|
|
3394
|
+
| [project-based.md](rules/video/project-based.md) | **Copy real components.** Eject actual UI code, strip logic, animate pixel-perfect |
|
|
3395
|
+
| [parameterization.md](rules/video/parameterization.md) | **Saves debugging.** Never hardcode frame numbers |
|
|
3396
|
+
| [layers.md](rules/video/layers.md) | **Prevents z-index bugs.** Background orbs \u2192 Vignette \u2192 CameraRig \u2192 Content |
|
|
3397
|
+
| [social-media.md](rules/video/social-media.md) | **Platform specs.** Resolution, aspect ratio, duration per platform |
|
|
3359
3398
|
|
|
3360
|
-
|
|
3399
|
+
**Required:** \`npx skills add https://github.com/remotion-dev/skills --skill remotion-best-practices\`
|
|
3361
3400
|
|
|
3362
|
-
|
|
3401
|
+
### Workflow
|
|
3363
3402
|
|
|
3364
|
-
|
|
3365
|
-
{
|
|
3366
|
-
"questions": [
|
|
3367
|
-
{
|
|
3368
|
-
"question": "Which voice would you prefer for the narration?",
|
|
3369
|
-
"header": "Voice",
|
|
3370
|
-
"options": [
|
|
3371
|
-
{ "label": "Kore (Recommended)", "description": "Female, professional voice - best for narration" },
|
|
3372
|
-
{ "label": "Puck", "description": "Male, energetic voice - good for promos" },
|
|
3373
|
-
{ "label": "Rachel", "description": "Female, calm voice" },
|
|
3374
|
-
{ "label": "No voiceover", "description": "Music only, no narration" }
|
|
3375
|
-
],
|
|
3376
|
-
"multiSelect": false
|
|
3377
|
-
},
|
|
3378
|
-
{
|
|
3379
|
-
"question": "What background music style fits your video?",
|
|
3380
|
-
"header": "Music",
|
|
3381
|
-
"options": [
|
|
3382
|
-
{ "label": "Uplifting/positive", "description": "Energetic and inspiring" },
|
|
3383
|
-
{ "label": "Corporate/professional", "description": "Modern, polished business feel" },
|
|
3384
|
-
{ "label": "Cinematic/dramatic", "description": "Epic, impactful presentation" },
|
|
3385
|
-
{ "label": "Calm ambient", "description": "Soft, subtle background" }
|
|
3386
|
-
],
|
|
3387
|
-
"multiSelect": false
|
|
3388
|
-
},
|
|
3389
|
-
{
|
|
3390
|
-
"question": "How long should the video be?",
|
|
3391
|
-
"header": "Duration",
|
|
3392
|
-
"options": [
|
|
3393
|
-
{ "label": "15 seconds", "description": "Quick teaser" },
|
|
3394
|
-
{ "label": "30 seconds", "description": "Social media friendly" },
|
|
3395
|
-
{ "label": "60 seconds", "description": "Standard length" }
|
|
3396
|
-
],
|
|
3397
|
-
"multiSelect": false
|
|
3398
|
-
}
|
|
3399
|
-
]
|
|
3400
|
-
}
|
|
3401
|
-
\`\`\`
|
|
3403
|
+
#### Phase 1: Discovery
|
|
3402
3404
|
|
|
3403
|
-
|
|
3405
|
+
Explore the project thoroughly - assets, components, branding, what makes this product/topic unique.
|
|
3404
3406
|
|
|
3405
|
-
|
|
3407
|
+
#### Phase 2: Video Brief
|
|
3406
3408
|
|
|
3407
|
-
|
|
3409
|
+
Present a brief outline (scenes, duration, assets found) and get user approval before production.
|
|
3408
3410
|
|
|
3409
|
-
|
|
3411
|
+
#### Phase 3: Production
|
|
3410
3412
|
|
|
3411
|
-
|
|
3413
|
+
1. **Read video-creation-guide.md + project-based.md + remotion-best-practices**
|
|
3414
|
+
2. **Generate audio** - \`${ctx.cmd} video create\` with scenes JSON
|
|
3415
|
+
3. **Scaffold OUTSIDE project** - \`cd .. && ${ctx.cmd} video init my-video\`
|
|
3416
|
+
4. **Copy assets** - Logo, fonts, colors from project into video project
|
|
3417
|
+
5. **Implement kinetically** - Camera rig, 2.5D entrances, staggered lists, transitions
|
|
3418
|
+
6. **Verify checklist** - Is it kinetic or static?
|
|
3412
3419
|
|
|
3413
|
-
|
|
3420
|
+
#### Phase 4: Render
|
|
3414
3421
|
|
|
3415
|
-
|
|
3422
|
+
Auto-render when done: \`pnpm exec remotion render FullVideo\`
|
|
3416
3423
|
|
|
3417
|
-
|
|
3418
|
-
${cmd2} video create \\
|
|
3419
|
-
--script "Your narration script..." \\
|
|
3420
|
-
--topic "topic for image search" \\
|
|
3421
|
-
--voice Kore \\
|
|
3422
|
-
--music-prompt "uplifting corporate" \\
|
|
3423
|
-
--num-images 5 \\
|
|
3424
|
-
--output ./public
|
|
3425
|
-
\`\`\`
|
|
3424
|
+
Only open Studio if user asks.
|
|
3426
3425
|
|
|
3427
|
-
|
|
3428
|
-
- \`public/audio/voiceover.wav\` - TTS voiceover (determines total duration)
|
|
3429
|
-
- \`public/audio/music.mp3\` - Background music (auto-matches voiceover length)
|
|
3430
|
-
- \`public/images/scene-*.jpg\` - Stock images
|
|
3431
|
-
- \`public/video-manifest.json\` - **Contains sections with exact TTS timestamps**
|
|
3426
|
+
### Kinetic Checklist
|
|
3432
3427
|
|
|
3433
|
-
|
|
3428
|
+
- [ ] CameraRig wraps entire scene with drift/zoom
|
|
3429
|
+
- [ ] Every UI element uses 2.5D rotation entrance (\`perspective + rotateX\`)
|
|
3430
|
+
- [ ] Cursor moves in Bezier curves with overshoot
|
|
3431
|
+
- [ ] Lists/grids stagger (never appear all at once)
|
|
3432
|
+
- [ ] Text uses masked reveal or keyword animation
|
|
3433
|
+
- [ ] Background has moving orbs + vignette + noise
|
|
3434
|
+
- [ ] Something is moving on EVERY frame
|
|
3435
|
+
- [ ] No static resting states longer than 30 frames
|
|
3434
3436
|
|
|
3435
|
-
|
|
3437
|
+
### Asset Generation
|
|
3436
3438
|
|
|
3437
|
-
\`\`\`
|
|
3439
|
+
\`\`\`bash
|
|
3440
|
+
cat <<EOF | ${ctx.cmd} video create --output ./public
|
|
3438
3441
|
{
|
|
3439
|
-
"
|
|
3440
|
-
"
|
|
3441
|
-
"
|
|
3442
|
-
"
|
|
3443
|
-
"characters": ["P", "u", "e", "r", "t", "o", " ", ...],
|
|
3444
|
-
"characterStartTimesSeconds": [0, 0.05, 0.1, ...],
|
|
3445
|
-
"characterEndTimesSeconds": [0.05, 0.1, 0.15, ...]
|
|
3446
|
-
}
|
|
3447
|
-
},
|
|
3448
|
-
"sections": [
|
|
3449
|
-
{
|
|
3450
|
-
"id": 1,
|
|
3451
|
-
"text": "Puerto Rico. La Isla del Encanto.",
|
|
3452
|
-
"wordCount": 5,
|
|
3453
|
-
"startTime": 0,
|
|
3454
|
-
"endTime": 2.8,
|
|
3455
|
-
"durationInSeconds": 2.8,
|
|
3456
|
-
"durationInFrames": 84,
|
|
3457
|
-
"imagePath": "images/scene-1.jpg"
|
|
3458
|
-
},
|
|
3459
|
-
{
|
|
3460
|
-
"id": 2,
|
|
3461
|
-
"text": "Discover five hundred years of history.",
|
|
3462
|
-
"wordCount": 7,
|
|
3463
|
-
"startTime": 2.8,
|
|
3464
|
-
"endTime": 8.2,
|
|
3465
|
-
"durationInSeconds": 5.4,
|
|
3466
|
-
"durationInFrames": 162,
|
|
3467
|
-
"imagePath": "images/scene-2.jpg"
|
|
3468
|
-
}
|
|
3442
|
+
"scenes": [
|
|
3443
|
+
{ "name": "Hook", "script": "..." },
|
|
3444
|
+
{ "name": "Demo", "script": "..." },
|
|
3445
|
+
{ "name": "CTA", "script": "..." }
|
|
3469
3446
|
],
|
|
3470
|
-
"
|
|
3471
|
-
"
|
|
3447
|
+
"voice": "Kore",
|
|
3448
|
+
"musicPrompt": "upbeat corporate"
|
|
3472
3449
|
}
|
|
3473
|
-
|
|
3450
|
+
EOF
|
|
3451
|
+
\`\`\``
|
|
3452
|
+
};
|
|
3474
3453
|
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3454
|
+
// src/commands/skill/generate-content.ts
|
|
3455
|
+
var DEFAULT_SECTIONS = [
|
|
3456
|
+
frontmatter,
|
|
3457
|
+
header3,
|
|
3458
|
+
prerequisites,
|
|
3459
|
+
workflow,
|
|
3460
|
+
createCommand2,
|
|
3461
|
+
createOptions,
|
|
3462
|
+
otherCommands,
|
|
3463
|
+
examples,
|
|
3464
|
+
output,
|
|
3465
|
+
videoCreation,
|
|
3466
|
+
bestPractices,
|
|
3467
|
+
fileTypes,
|
|
3468
|
+
troubleshooting
|
|
3469
|
+
];
|
|
3470
|
+
function createSkillContext(brand2) {
|
|
3471
|
+
return {
|
|
3472
|
+
cmd: brand2.name,
|
|
3473
|
+
pkg: brand2.packageName,
|
|
3474
|
+
url: brand2.apiUrl,
|
|
3475
|
+
name: brand2.displayName
|
|
3476
|
+
};
|
|
3477
|
+
}
|
|
3478
|
+
function generateSkillContent(brand2, sections = DEFAULT_SECTIONS) {
|
|
3479
|
+
const ctx = createSkillContext(brand2);
|
|
3480
|
+
return sections.map((section) => section.render(ctx)).join("\n\n");
|
|
3481
|
+
}
|
|
3479
3482
|
|
|
3480
|
-
|
|
3483
|
+
// src/commands/skill/installer.ts
|
|
3484
|
+
import { mkdirSync, writeFileSync, existsSync as existsSync2, rmSync } from "fs";
|
|
3485
|
+
import { join, resolve as resolve4, relative } from "path";
|
|
3486
|
+
import { homedir } from "os";
|
|
3487
|
+
|
|
3488
|
+
// src/commands/skill/editors.ts
|
|
3489
|
+
var SUPPORTED_EDITORS = [
|
|
3490
|
+
{ name: "Claude Code", dir: ".claude" },
|
|
3491
|
+
{ name: "Cursor", dir: ".cursor" },
|
|
3492
|
+
{ name: "Codex", dir: ".codex" },
|
|
3493
|
+
{ name: "OpenCode", dir: ".opencode" },
|
|
3494
|
+
{ name: "Windsurf", dir: ".windsurf" },
|
|
3495
|
+
{ name: "Agent", dir: ".agent" }
|
|
3496
|
+
];
|
|
3481
3497
|
|
|
3482
|
-
|
|
3498
|
+
// src/commands/skill/rules/video/content.ts
|
|
3499
|
+
var VIDEO_RULE_CONTENTS = [
|
|
3500
|
+
{
|
|
3501
|
+
filename: "video-creation-guide.md",
|
|
3502
|
+
content: `# Video Creation Guide
|
|
3483
3503
|
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3504
|
+
> **ALSO READ:**
|
|
3505
|
+
> - [project-based.md](project-based.md) - How to extract and animate real project components pixel-perfect
|
|
3506
|
+
> - If installed, check \`remotion-best-practices\` skill for Remotion-specific patterns
|
|
3487
3507
|
|
|
3488
|
-
|
|
3489
|
-
export const SECTION_1_DURATION = manifest.sections[0].durationInFrames; // 84
|
|
3490
|
-
export const SECTION_2_DURATION = manifest.sections[1].durationInFrames; // 162
|
|
3491
|
-
// ... etc
|
|
3508
|
+
---
|
|
3492
3509
|
|
|
3493
|
-
|
|
3494
|
-
\`\`\`
|
|
3510
|
+
# The "Kinetic SaaS" Motion Design System
|
|
3495
3511
|
|
|
3496
|
-
|
|
3512
|
+
**Objective:** Replicate the high-energy, fluid feel of premium tech product videos (Stripe, Apple, Linear, Affable.ai).
|
|
3497
3513
|
|
|
3498
|
-
|
|
3499
|
-
// src/remotion/scenes/Scene1.tsx
|
|
3500
|
-
import { AbsoluteFill, Img, staticFile, useCurrentFrame, useVideoConfig, spring } from "remotion";
|
|
3501
|
-
import manifest from '../../../public/video-manifest.json';
|
|
3514
|
+
**Core Philosophy:** "Nothing sits still. Everything is physics-based. Every pixel breathes."
|
|
3502
3515
|
|
|
3503
|
-
|
|
3504
|
-
export const SCENE_1_DURATION = section.durationInFrames;
|
|
3516
|
+
This is the difference between **Static Composition** (slideshows) and **Kinetic Composition** (motion graphics). You must rebuild UI as individual animated layers and film with a virtual camera.
|
|
3505
3517
|
|
|
3506
|
-
|
|
3507
|
-
const frame = useCurrentFrame();
|
|
3508
|
-
const { fps } = useVideoConfig();
|
|
3509
|
-
const progress = spring({ frame, fps, config: { damping: 15, stiffness: 100 } });
|
|
3518
|
+
---
|
|
3510
3519
|
|
|
3511
|
-
|
|
3512
|
-
<AbsoluteFill>
|
|
3513
|
-
<Img src={staticFile(section.imagePath)} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
|
|
3514
|
-
<div style={{
|
|
3515
|
-
position: 'absolute',
|
|
3516
|
-
bottom: 100,
|
|
3517
|
-
left: 0,
|
|
3518
|
-
right: 0,
|
|
3519
|
-
textAlign: 'center',
|
|
3520
|
-
opacity: progress,
|
|
3521
|
-
transform: \`translateY(\${(1 - progress) * 20}px)\`,
|
|
3522
|
-
}}>
|
|
3523
|
-
<h1 style={{ color: 'white', fontSize: 60, textShadow: '2px 2px 8px rgba(0,0,0,0.8)' }}>
|
|
3524
|
-
{section.text}
|
|
3525
|
-
</h1>
|
|
3526
|
-
</div>
|
|
3527
|
-
</AbsoluteFill>
|
|
3528
|
-
);
|
|
3529
|
-
};
|
|
3530
|
-
\`\`\`
|
|
3520
|
+
## 1. The Global Camera Rig (The "Anti-Static" Layer)
|
|
3531
3521
|
|
|
3532
|
-
|
|
3522
|
+
Even when reading text, the screen is slowly zooming or panning.
|
|
3533
3523
|
|
|
3534
|
-
|
|
3535
|
-
import { AbsoluteFill, Series, Audio, staticFile, useCurrentFrame, interpolate } from "remotion";
|
|
3536
|
-
import manifest from '../../public/video-manifest.json';
|
|
3537
|
-
import { Scene1, SCENE_1_DURATION } from "./scenes/Scene1";
|
|
3538
|
-
import { Scene2, SCENE_2_DURATION } from "./scenes/Scene2";
|
|
3539
|
-
// ... import all scenes
|
|
3524
|
+
### Virtual Camera
|
|
3540
3525
|
|
|
3541
|
-
|
|
3526
|
+
Every scene must be wrapped in a \`CameraRig\` component:
|
|
3542
3527
|
|
|
3543
|
-
|
|
3528
|
+
\`\`\`tsx
|
|
3529
|
+
const CameraRig: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|
3544
3530
|
const frame = useCurrentFrame();
|
|
3545
|
-
const
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3531
|
+
const { durationInFrames } = useVideoConfig();
|
|
3532
|
+
|
|
3533
|
+
// The "Drift" - constant subtle movement
|
|
3534
|
+
const scale = interpolate(frame, [0, durationInFrames], [1.0, 1.05]);
|
|
3535
|
+
const rotation = interpolate(frame, [0, durationInFrames], [0, 0.5]);
|
|
3549
3536
|
|
|
3550
|
-
export const FullVideo: React.FC = () => {
|
|
3551
3537
|
return (
|
|
3552
|
-
<AbsoluteFill
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
</Series.Sequence>
|
|
3557
|
-
<Series.Sequence durationInFrames={SCENE_2_DURATION}>
|
|
3558
|
-
<Scene2 />
|
|
3559
|
-
</Series.Sequence>
|
|
3560
|
-
{/* Add all sections */}
|
|
3561
|
-
</Series>
|
|
3562
|
-
|
|
3563
|
-
<Audio src={staticFile("audio/voiceover.wav")} volume={1} />
|
|
3564
|
-
<BackgroundMusic />
|
|
3538
|
+
<AbsoluteFill style={{
|
|
3539
|
+
transform: \`scale(\${scale}) rotate(\${rotation}deg)\`,
|
|
3540
|
+
}}>
|
|
3541
|
+
{children}
|
|
3565
3542
|
</AbsoluteFill>
|
|
3566
3543
|
);
|
|
3567
3544
|
};
|
|
3568
3545
|
\`\`\`
|
|
3569
3546
|
|
|
3570
|
-
###
|
|
3571
|
-
|
|
3572
|
-
\`\`\`bash
|
|
3573
|
-
npm run dev # Preview in Remotion Studio
|
|
3574
|
-
npm run render # Output to out/video.mp4
|
|
3575
|
-
\`\`\`
|
|
3576
|
-
|
|
3577
|
-
## CLI Commands Reference
|
|
3578
|
-
|
|
3579
|
-
### ${cmd2} video create
|
|
3580
|
-
|
|
3581
|
-
| Option | Required | Default | Description |
|
|
3582
|
-
|--------|----------|---------|-------------|
|
|
3583
|
-
| \`-s, --script <text>\` | Yes* | - | Narration script |
|
|
3584
|
-
| \`--script-file <path>\` | Yes* | - | Path to script file |
|
|
3585
|
-
| \`-t, --topic <text>\` | No | auto | Topic for image search |
|
|
3586
|
-
| \`-v, --voice <name>\` | No | Kore | TTS voice |
|
|
3587
|
-
| \`-m, --music-prompt <text>\` | No | auto | Music description |
|
|
3588
|
-
| \`-n, --num-images <n>\` | No | 5 | Number of images |
|
|
3589
|
-
| \`-o, --output <dir>\` | No | ./public | Output directory |
|
|
3547
|
+
### Zoom-Through Transitions
|
|
3590
3548
|
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
\`\`\`bash
|
|
3594
|
-
${cmd2} tts generate -t "Narration text" -o voice.wav --voice Kore
|
|
3595
|
-
${cmd2} tts voices # List all voices
|
|
3596
|
-
\`\`\`
|
|
3549
|
+
Transitions are NOT just fades - they are camera movements:
|
|
3597
3550
|
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
\`\`\`bash
|
|
3603
|
-
${cmd2} music generate -p "uplifting corporate" -d 30 -o music.mp3
|
|
3551
|
+
\`\`\`tsx
|
|
3552
|
+
// Camera scales rapidly into an element (logo, dot) until it fills screen
|
|
3553
|
+
const zoomThrough = interpolate(frame, [TRANSITION_START, TRANSITION_END], [1, 50]);
|
|
3554
|
+
// The element becomes the background for the next scene
|
|
3604
3555
|
\`\`\`
|
|
3605
3556
|
|
|
3606
|
-
|
|
3557
|
+
---
|
|
3607
3558
|
|
|
3608
|
-
|
|
3559
|
+
## 2. UI & Mockup Animation Rules (Rebuilding Reality)
|
|
3609
3560
|
|
|
3610
|
-
|
|
3611
|
-
${cmd2} image search -q "tropical beach" -n 5 -s large
|
|
3612
|
-
${cmd2} video search "tech workspace" -n 5
|
|
3613
|
-
\`\`\`
|
|
3561
|
+
UI doesn't just fade in. It pops up with weight.
|
|
3614
3562
|
|
|
3615
|
-
###
|
|
3563
|
+
### Priority: Use Real Project Components
|
|
3616
3564
|
|
|
3617
|
-
|
|
3565
|
+
**If you're in a project folder, ALWAYS check for actual components first:**
|
|
3618
3566
|
|
|
3619
3567
|
\`\`\`bash
|
|
3620
|
-
|
|
3568
|
+
# Before building ANY UI, explore the project
|
|
3569
|
+
ls src/components/
|
|
3570
|
+
ls src/app/
|
|
3621
3571
|
\`\`\`
|
|
3622
3572
|
|
|
3623
|
-
|
|
3573
|
+
**When project components exist:**
|
|
3574
|
+
1. **Copy the actual component** into your Remotion project
|
|
3575
|
+
2. **Strip logic** (remove useState, API calls, event handlers)
|
|
3576
|
+
3. **Keep visuals identical** (same styles, colors, spacing)
|
|
3577
|
+
4. **Add animation props** (progress, isHovered, etc.)
|
|
3578
|
+
5. **Apply kinetic animation** using the rules below
|
|
3624
3579
|
|
|
3625
|
-
|
|
3626
|
-
|---------|--------|-------|
|
|
3627
|
-
| Voiceover | 100% | Primary audio |
|
|
3628
|
-
| Background music | 20-30% | Fade in/out over ~10-20 frames |
|
|
3580
|
+
See [project-based.md](project-based.md) for the full component extraction process.
|
|
3629
3581
|
|
|
3630
|
-
|
|
3582
|
+
**This is the priority order:**
|
|
3583
|
+
1. \u2705 Pixel-perfect copy of real project components (animated)
|
|
3584
|
+
2. \u26A0\uFE0F Recreate UI from project's design system (colors, fonts, spacing)
|
|
3585
|
+
3. \u274C Generic mockups or stock images (ONLY for non-UI scenes like hooks)
|
|
3631
3586
|
|
|
3632
|
-
|
|
3587
|
+
### The "Pop-Up" Entrance (2.5D Rotation)
|
|
3633
3588
|
|
|
3634
|
-
|
|
3589
|
+
\`\`\`tsx
|
|
3590
|
+
// Heavy spring for weighty feel
|
|
3591
|
+
const progress = spring({
|
|
3592
|
+
frame,
|
|
3593
|
+
fps,
|
|
3594
|
+
config: { mass: 2, damping: 20, stiffness: 100 },
|
|
3595
|
+
});
|
|
3635
3596
|
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3597
|
+
// Start tilted back, spring to flat
|
|
3598
|
+
const rotateX = interpolate(progress, [0, 1], [20, 0]);
|
|
3599
|
+
const y = interpolate(progress, [0, 1], [100, 0]);
|
|
3600
|
+
const scale = interpolate(progress, [0, 1], [0.8, 1.0]);
|
|
3640
3601
|
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3602
|
+
<div style={{
|
|
3603
|
+
transform: \`perspective(1000px) rotateX(\${rotateX}deg) translateY(\${y}px) scale(\${scale})\`,
|
|
3604
|
+
}}>
|
|
3605
|
+
{uiComponent}
|
|
3606
|
+
</div>
|
|
3607
|
+
\`\`\`
|
|
3644
3608
|
|
|
3645
|
-
|
|
3609
|
+
### Cursor Simulation
|
|
3646
3610
|
|
|
3647
|
-
|
|
3611
|
+
**Movement:** Never linear. Cursors move in **Bezier Curves** with an arc.
|
|
3648
3612
|
|
|
3649
|
-
|
|
3613
|
+
\`\`\`tsx
|
|
3614
|
+
// 1. Spring progress from start to end
|
|
3615
|
+
const progress = spring({
|
|
3616
|
+
frame: frame - startFrame,
|
|
3617
|
+
fps,
|
|
3618
|
+
config: { damping: 20, stiffness: 80 }, // Slower, deliberate
|
|
3619
|
+
});
|
|
3650
3620
|
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
find public -name "*logo*" 2>/dev/null
|
|
3621
|
+
// 2. Linear interpolation for X and Y
|
|
3622
|
+
const linearX = interpolate(progress, [0, 1], [start.x, end.x]);
|
|
3623
|
+
const linearY = interpolate(progress, [0, 1], [start.y, end.y]);
|
|
3655
3624
|
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3625
|
+
// 3. THE ARC: Parabola that peaks mid-travel (this is the secret sauce)
|
|
3626
|
+
const arcHeight = 100; // How much the cursor "loops"
|
|
3627
|
+
const arcOffset = Math.sin(progress * Math.PI) * arcHeight;
|
|
3659
3628
|
|
|
3660
|
-
|
|
3661
|
-
|
|
3629
|
+
// 4. Apply arc to Y position
|
|
3630
|
+
const cursorY = linearY - arcOffset;
|
|
3662
3631
|
\`\`\`
|
|
3663
3632
|
|
|
3664
|
-
|
|
3633
|
+
**Click Interaction:**
|
|
3634
|
+
\`\`\`tsx
|
|
3635
|
+
// On click:
|
|
3636
|
+
// 1. Cursor scales down
|
|
3637
|
+
const cursorScale = isClicking ? 0.8 : 1;
|
|
3665
3638
|
|
|
3666
|
-
|
|
3639
|
+
// 2. Button squishes
|
|
3640
|
+
const buttonScaleX = isClicking ? 1.05 : 1;
|
|
3641
|
+
const buttonScaleY = isClicking ? 0.95 : 1;
|
|
3667
3642
|
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
find src/components -name "*.tsx" | head -30
|
|
3643
|
+
// 3. Release both with spring
|
|
3644
|
+
\`\`\`
|
|
3671
3645
|
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3646
|
+
**Standard Cursor SVG:**
|
|
3647
|
+
\`\`\`tsx
|
|
3648
|
+
// Mac-style cursor (use as reference, customize as needed)
|
|
3649
|
+
<svg width="32" height="32" viewBox="0 0 32 32" fill="none">
|
|
3650
|
+
<path
|
|
3651
|
+
d="M4 4L11.5 26L16 17.5L25 25L27 23L18.5 15L28 11.5L4 4Z"
|
|
3652
|
+
fill="black"
|
|
3653
|
+
stroke="white"
|
|
3654
|
+
strokeWidth="2"
|
|
3655
|
+
/>
|
|
3656
|
+
</svg>
|
|
3676
3657
|
\`\`\`
|
|
3677
3658
|
|
|
3678
|
-
|
|
3679
|
-
- Layout structure (sidebar? grid? list?)
|
|
3680
|
-
- Colors used (bg-slate-900, text-teal-400, etc.)
|
|
3681
|
-
- Visual elements (badges, icons, thumbnails)
|
|
3682
|
-
- Typography (font sizes, weights)
|
|
3659
|
+
### Staggered Lists & Grids
|
|
3683
3660
|
|
|
3684
|
-
|
|
3661
|
+
**Rule:** NEVER show a list or grid all at once.
|
|
3685
3662
|
|
|
3686
|
-
\`\`\`
|
|
3687
|
-
|
|
3663
|
+
\`\`\`tsx
|
|
3664
|
+
const STAGGER_FRAMES = 4; // ~0.05s at 60fps
|
|
3688
3665
|
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3666
|
+
{items.map((item, i) => {
|
|
3667
|
+
const delay = i * STAGGER_FRAMES;
|
|
3668
|
+
const progress = spring({
|
|
3669
|
+
frame: frame - delay,
|
|
3670
|
+
fps,
|
|
3671
|
+
config: { damping: 15, stiffness: 120 },
|
|
3672
|
+
});
|
|
3695
3673
|
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3674
|
+
return (
|
|
3675
|
+
<div style={{
|
|
3676
|
+
opacity: progress,
|
|
3677
|
+
transform: \`translateY(\${interpolate(progress, [0, 1], [20, 0])}px)\`,
|
|
3678
|
+
}}>
|
|
3679
|
+
{item}
|
|
3680
|
+
</div>
|
|
3681
|
+
);
|
|
3682
|
+
})}
|
|
3700
3683
|
\`\`\`
|
|
3701
3684
|
|
|
3702
3685
|
---
|
|
3703
3686
|
|
|
3704
|
-
##
|
|
3705
|
-
|
|
3706
|
-
### Scene Structure
|
|
3687
|
+
## 3. Kinetic Typography (Text that Hits)
|
|
3707
3688
|
|
|
3708
|
-
|
|
3709
|
-
## Video Plan: [App Name] Demo
|
|
3689
|
+
Text doesn't fade. It slams in, changes fill, or slides up.
|
|
3710
3690
|
|
|
3711
|
-
###
|
|
3712
|
-
**What to show:** Logo + tagline on dark background
|
|
3713
|
-
**Colors:** bg #0f172a, logo centered
|
|
3714
|
-
**Animation:** Logo scales in with spring, tagline fades up
|
|
3691
|
+
### Pattern A: The "Masked Reveal"
|
|
3715
3692
|
|
|
3716
|
-
|
|
3717
|
-
**What to show:** Replicate the slides sidebar
|
|
3718
|
-
**Reference:** Read src/components/slides/SlidesSidebar.tsx
|
|
3719
|
-
**Build:** Dark sidebar with slide items, thumbnails
|
|
3720
|
-
**Animation:** Sidebar slides in, items stagger
|
|
3693
|
+
Text rises from a floor:
|
|
3721
3694
|
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3695
|
+
\`\`\`tsx
|
|
3696
|
+
<div style={{ overflow: 'hidden', height: 80 }}>
|
|
3697
|
+
<h1 style={{
|
|
3698
|
+
transform: \`translateY(\${interpolate(progress, [0, 1], [100, 0])}%)\`,
|
|
3699
|
+
}}>
|
|
3700
|
+
INTRODUCING
|
|
3701
|
+
</h1>
|
|
3702
|
+
</div>
|
|
3730
3703
|
\`\`\`
|
|
3731
3704
|
|
|
3732
|
-
|
|
3705
|
+
### Pattern B: Variable Weight (Animate Keywords)
|
|
3733
3706
|
|
|
3734
|
-
|
|
3707
|
+
Don't animate the whole sentence. Animate **keywords**:
|
|
3735
3708
|
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3709
|
+
\`\`\`tsx
|
|
3710
|
+
// "Teams waste HOURS" - only "HOURS" animates
|
|
3711
|
+
<p>
|
|
3712
|
+
Teams waste{' '}
|
|
3713
|
+
<span style={{
|
|
3714
|
+
transform: \`scale(\${interpolate(keywordProgress, [0, 1], [1, 1.2])})\`,
|
|
3715
|
+
color: interpolateColors(keywordProgress, [0, 1], ['#F0F0F0', '#FF6B6B']),
|
|
3716
|
+
textShadow: \`0 0 \${glowProgress * 20}px rgba(255,107,107,0.5)\`,
|
|
3717
|
+
}}>
|
|
3718
|
+
HOURS
|
|
3719
|
+
</span>
|
|
3720
|
+
</p>
|
|
3742
3721
|
\`\`\`
|
|
3743
3722
|
|
|
3744
|
-
###
|
|
3723
|
+
### Pattern C: The "Glitch/Tech" Accent
|
|
3745
3724
|
|
|
3746
|
-
|
|
3747
|
-
# Logo
|
|
3748
|
-
cp ../myapp/public/logo.svg ./public/
|
|
3749
|
-
|
|
3750
|
-
# Tailwind config (for colors/fonts)
|
|
3751
|
-
cp ../myapp/tailwind.config.* ./
|
|
3725
|
+
Chromatic aberration on impact:
|
|
3752
3726
|
|
|
3753
|
-
|
|
3754
|
-
|
|
3727
|
+
\`\`\`tsx
|
|
3728
|
+
// Duplicate text, offset by 2px with color channels, flash for 2 frames
|
|
3729
|
+
{isImpactFrame && (
|
|
3730
|
+
<>
|
|
3731
|
+
<span style={{ position: 'absolute', left: -2, color: '#FF0000', opacity: 0.5 }}>TEXT</span>
|
|
3732
|
+
<span style={{ position: 'absolute', left: 2, color: '#0000FF', opacity: 0.5 }}>TEXT</span>
|
|
3733
|
+
</>
|
|
3734
|
+
)}
|
|
3755
3735
|
\`\`\`
|
|
3756
3736
|
|
|
3757
|
-
###
|
|
3737
|
+
### Text Colors
|
|
3758
3738
|
|
|
3759
|
-
|
|
3739
|
+
Never pure white (\`#FFF\`). Use \`#F0F0F0\` with subtle gradient or shadow for depth.
|
|
3760
3740
|
|
|
3761
|
-
|
|
3762
|
-
// src/remotion/scenes/SidebarScene.tsx
|
|
3763
|
-
// Replicates: src/components/slides/SlidesSidebar.tsx
|
|
3741
|
+
---
|
|
3764
3742
|
|
|
3765
|
-
|
|
3766
|
-
import { AbsoluteFill, useCurrentFrame, spring, useVideoConfig } from "remotion";
|
|
3743
|
+
## 4. Atmosphere & Backgrounds (The "Deep Space")
|
|
3767
3744
|
|
|
3768
|
-
|
|
3769
|
-
{ id: 1, title: "Title Slide", selected: true },
|
|
3770
|
-
{ id: 2, title: "Overview", selected: false },
|
|
3771
|
-
{ id: 3, title: "Key Players", selected: false },
|
|
3772
|
-
];
|
|
3745
|
+
Background is never a solid color. It's deep black with floating colored orbs.
|
|
3773
3746
|
|
|
3774
|
-
|
|
3747
|
+
### The "Orb" System
|
|
3775
3748
|
|
|
3776
|
-
|
|
3749
|
+
\`\`\`tsx
|
|
3750
|
+
const MovingBackground: React.FC = () => {
|
|
3777
3751
|
const frame = useCurrentFrame();
|
|
3778
|
-
const { fps } = useVideoConfig();
|
|
3779
3752
|
|
|
3780
|
-
|
|
3781
|
-
const
|
|
3753
|
+
// Orbs move in figure-8 or circular patterns
|
|
3754
|
+
const orb1X = Math.sin(frame / 60) * 200;
|
|
3755
|
+
const orb1Y = Math.cos(frame / 80) * 100;
|
|
3756
|
+
const orb2X = Math.sin(frame / 70 + Math.PI) * 150;
|
|
3757
|
+
const orb2Y = Math.cos(frame / 90 + Math.PI) * 120;
|
|
3782
3758
|
|
|
3783
3759
|
return (
|
|
3784
|
-
<AbsoluteFill style={{ backgroundColor:
|
|
3785
|
-
{/*
|
|
3760
|
+
<AbsoluteFill style={{ backgroundColor: '#0a0a0a' }}>
|
|
3761
|
+
{/* Orb 1 - Teal */}
|
|
3786
3762
|
<div style={{
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
<div key={slide.id} style={{
|
|
3811
|
-
opacity: itemProgress,
|
|
3812
|
-
transform: \`translateX(\${(1 - itemProgress) * -20}px)\`,
|
|
3813
|
-
marginBottom: 8,
|
|
3814
|
-
padding: 12,
|
|
3815
|
-
borderRadius: 8,
|
|
3816
|
-
backgroundColor: slide.selected ? "#1e293b" : "transparent",
|
|
3817
|
-
border: slide.selected ? "1px solid #14b8a6" : "1px solid transparent",
|
|
3818
|
-
}}>
|
|
3819
|
-
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
|
|
3820
|
-
<div style={{ width: 48, height: 32, backgroundColor: "#334155", borderRadius: 4 }} />
|
|
3821
|
-
<div>
|
|
3822
|
-
<span style={{ color: "#64748b", fontSize: 12 }}>
|
|
3823
|
-
SLIDE {String(i + 1).padStart(2, "0")}
|
|
3824
|
-
</span>
|
|
3825
|
-
{slide.selected && <span style={{ color: "#f87171", fontSize: 12, marginLeft: 8 }}>SELECTED</span>}
|
|
3826
|
-
<p style={{ color: "#ffffff", fontSize: 14, margin: 0 }}>{slide.title}</p>
|
|
3827
|
-
</div>
|
|
3828
|
-
</div>
|
|
3829
|
-
</div>
|
|
3830
|
-
);
|
|
3831
|
-
})}
|
|
3832
|
-
</div>
|
|
3763
|
+
position: 'absolute',
|
|
3764
|
+
width: 600,
|
|
3765
|
+
height: 600,
|
|
3766
|
+
borderRadius: '50%',
|
|
3767
|
+
background: 'radial-gradient(circle, rgba(20,184,166,0.3) 0%, transparent 70%)',
|
|
3768
|
+
filter: 'blur(100px)',
|
|
3769
|
+
transform: \`translate(\${orb1X}px, \${orb1Y}px)\`,
|
|
3770
|
+
left: '20%',
|
|
3771
|
+
top: '30%',
|
|
3772
|
+
}} />
|
|
3773
|
+
|
|
3774
|
+
{/* Orb 2 - Purple */}
|
|
3775
|
+
<div style={{
|
|
3776
|
+
position: 'absolute',
|
|
3777
|
+
width: 500,
|
|
3778
|
+
height: 500,
|
|
3779
|
+
borderRadius: '50%',
|
|
3780
|
+
background: 'radial-gradient(circle, rgba(168,85,247,0.3) 0%, transparent 70%)',
|
|
3781
|
+
filter: 'blur(100px)',
|
|
3782
|
+
transform: \`translate(\${orb2X}px, \${orb2Y}px)\`,
|
|
3783
|
+
right: '20%',
|
|
3784
|
+
bottom: '20%',
|
|
3785
|
+
}} />
|
|
3833
3786
|
</AbsoluteFill>
|
|
3834
3787
|
);
|
|
3835
3788
|
};
|
|
3836
3789
|
\`\`\`
|
|
3837
3790
|
|
|
3838
|
-
###
|
|
3839
|
-
|
|
3840
|
-
**The video UI should be indistinguishable from the real app.**
|
|
3791
|
+
### Vignette & Noise
|
|
3841
3792
|
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3793
|
+
\`\`\`tsx
|
|
3794
|
+
// Noise texture overlay (5% opacity)
|
|
3795
|
+
<AbsoluteFill style={{
|
|
3796
|
+
backgroundImage: 'url(/noise.png)',
|
|
3797
|
+
opacity: 0.05,
|
|
3798
|
+
mixBlendMode: 'overlay',
|
|
3799
|
+
}} />
|
|
3800
|
+
|
|
3801
|
+
// Vignette (dark corners)
|
|
3802
|
+
<AbsoluteFill style={{
|
|
3803
|
+
background: 'radial-gradient(ellipse at center, transparent 40%, rgba(0,0,0,0.8) 100%)',
|
|
3804
|
+
}} />
|
|
3805
|
+
\`\`\`
|
|
3848
3806
|
|
|
3849
3807
|
---
|
|
3850
3808
|
|
|
3851
|
-
##
|
|
3809
|
+
## 5. Physics & Timing Reference
|
|
3852
3810
|
|
|
3853
|
-
###
|
|
3811
|
+
### Spring Configs
|
|
3854
3812
|
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
\`\`\`
|
|
3813
|
+
| Use Case | Config | Feel |
|
|
3814
|
+
|----------|--------|------|
|
|
3815
|
+
| Standard (snappy) | \`{ mass: 1, damping: 15, stiffness: 120 }\` | Smooth, professional |
|
|
3816
|
+
| Heavy (UI mockups) | \`{ mass: 2, damping: 20, stiffness: 100 }\` | Weighty, premium |
|
|
3817
|
+
| Bouncy (attention) | \`{ mass: 1, damping: 10, stiffness: 150 }\` | Playful overshoot |
|
|
3861
3818
|
|
|
3862
|
-
###
|
|
3819
|
+
### Timing Values
|
|
3863
3820
|
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3821
|
+
| Action | Frames | Notes |
|
|
3822
|
+
|--------|--------|-------|
|
|
3823
|
+
| Element entrance | 15-20 | Spring to rest |
|
|
3824
|
+
| Stagger gap | 3-5 | Between list items |
|
|
3825
|
+
| Hold on key info | 45-60 | Minimum read time |
|
|
3826
|
+
| Scene transition | 20-30 | Zoom-through |
|
|
3827
|
+
|
|
3828
|
+
### The Golden Rule
|
|
3829
|
+
|
|
3830
|
+
Use \`interpolate(frame)\` to ensure \`scale\` or \`rotation\` is changing on **EVERY frame**. No static resting states.
|
|
3868
3831
|
|
|
3869
3832
|
---
|
|
3870
3833
|
|
|
3871
|
-
##
|
|
3834
|
+
## 6. Micro-Tricks from Premium Videos
|
|
3872
3835
|
|
|
3873
|
-
###
|
|
3874
|
-
\`\`\`tsx
|
|
3875
|
-
// \u274C NO
|
|
3876
|
-
<div style={{ background: "linear-gradient(#667eea, #764ba2)", width: 200, height: 150 }} />
|
|
3877
|
-
\`\`\`
|
|
3836
|
+
### The "Match Cut"
|
|
3878
3837
|
|
|
3879
|
-
|
|
3880
|
-
\`\`\`tsx
|
|
3881
|
-
// \u274C NO
|
|
3882
|
-
<h2>Key Insights from Research</h2>
|
|
3883
|
-
<li>Finding 1: Performance improved by 45%</li>
|
|
3884
|
-
\`\`\`
|
|
3838
|
+
Small circle zooms to fill screen, becomes next scene's background:
|
|
3885
3839
|
|
|
3886
|
-
### Bad: Not matching the app
|
|
3887
3840
|
\`\`\`tsx
|
|
3888
|
-
//
|
|
3889
|
-
|
|
3841
|
+
// Circle element scales 50x to wipe to next scene
|
|
3842
|
+
const circleScale = interpolate(frame, [MATCH_START, MATCH_END], [1, 50]);
|
|
3890
3843
|
\`\`\`
|
|
3891
3844
|
|
|
3892
|
-
###
|
|
3845
|
+
### Search Bar Typing
|
|
3846
|
+
|
|
3847
|
+
Include blinking cursor:
|
|
3848
|
+
|
|
3893
3849
|
\`\`\`tsx
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3850
|
+
const Typewriter: React.FC<{ text: string }> = ({ text }) => {
|
|
3851
|
+
const frame = useCurrentFrame();
|
|
3852
|
+
const charIndex = Math.floor(frame / 3); // 3 frames per char
|
|
3853
|
+
const showCursor = Math.floor(frame / 15) % 2 === 0; // Blink every 15 frames
|
|
3854
|
+
|
|
3855
|
+
return (
|
|
3856
|
+
<span>
|
|
3857
|
+
{text.slice(0, charIndex)}
|
|
3858
|
+
{showCursor && <span style={{ opacity: 0.8 }}>|</span>}
|
|
3859
|
+
</span>
|
|
3860
|
+
);
|
|
3861
|
+
};
|
|
3898
3862
|
\`\`\`
|
|
3899
3863
|
|
|
3900
|
-
|
|
3864
|
+
### The "Card Fan"
|
|
3901
3865
|
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
### Before Building
|
|
3905
|
-
- [ ] Read motion-standards.md and micro-interactions.md
|
|
3906
|
-
- [ ] Found logo path
|
|
3907
|
-
- [ ] Found colors from tailwind.config
|
|
3908
|
-
- [ ] Read key components to understand visual structure
|
|
3909
|
-
- [ ] Documented findings
|
|
3910
|
-
- [ ] Planned scenes
|
|
3911
|
-
|
|
3912
|
-
### While Building
|
|
3913
|
-
- [ ] Using exact colors from tailwind.config
|
|
3914
|
-
- [ ] Matching layout structure of real app
|
|
3915
|
-
- [ ] Using spring() for animations
|
|
3916
|
-
- [ ] Mock data is realistic
|
|
3917
|
-
|
|
3918
|
-
### Before Render
|
|
3919
|
-
- [ ] Logo appears in intro and CTA
|
|
3920
|
-
- [ ] Colors match the app exactly
|
|
3921
|
-
- [ ] All scenes have smooth animations
|
|
3922
|
-
`;
|
|
3923
|
-
}
|
|
3924
|
-
function generateMotionStandardsRule() {
|
|
3925
|
-
return `---
|
|
3926
|
-
name: motion-standards
|
|
3927
|
-
description: Animation quality standards for high-end video production
|
|
3928
|
-
---
|
|
3866
|
+
Stack cards, then fan out:
|
|
3929
3867
|
|
|
3930
|
-
|
|
3868
|
+
\`\`\`tsx
|
|
3869
|
+
const cards = [
|
|
3870
|
+
{ rotation: -5, x: -20 },
|
|
3871
|
+
{ rotation: 0, x: 0 },
|
|
3872
|
+
{ rotation: 5, x: 20 },
|
|
3873
|
+
];
|
|
3931
3874
|
|
|
3932
|
-
|
|
3875
|
+
{cards.map((card, i) => {
|
|
3876
|
+
const fanProgress = spring({ frame: frame - 30, fps, config: { damping: 15, stiffness: 100 } });
|
|
3933
3877
|
|
|
3934
|
-
|
|
3878
|
+
return (
|
|
3879
|
+
<div style={{
|
|
3880
|
+
position: 'absolute',
|
|
3881
|
+
transform: \`rotate(\${fanProgress * card.rotation}deg) translateX(\${fanProgress * card.x}px)\`,
|
|
3882
|
+
zIndex: i,
|
|
3883
|
+
}}>
|
|
3884
|
+
<ProfileCard />
|
|
3885
|
+
</div>
|
|
3886
|
+
);
|
|
3887
|
+
})}
|
|
3888
|
+
\`\`\`
|
|
3935
3889
|
|
|
3936
|
-
|
|
3890
|
+
---
|
|
3937
3891
|
|
|
3938
|
-
|
|
3939
|
-
- **Implementation:** Use \`spring()\` for ALL entrance/exit animations
|
|
3940
|
-
- **Default config:** \`{ mass: 0.8, stiffness: 150, damping: 15 }\`
|
|
3892
|
+
## 7. Strict Animation Guidelines (DO NOT DEVIATE)
|
|
3941
3893
|
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
const opacity = interpolate(frame, [0, 30], [0, 1]);
|
|
3894
|
+
**ROLE:** Senior Motion Graphics Engineer for Remotion.
|
|
3895
|
+
**REFERENCE STYLE:** "High-End SaaS Product Launch" (e.g., Affable.ai, Linear.app).
|
|
3945
3896
|
|
|
3946
|
-
|
|
3947
|
-
const progress = spring({ frame, fps, config: { mass: 0.8, stiffness: 150, damping: 15 } });
|
|
3948
|
-
\`\`\`
|
|
3897
|
+
### Physics & Timing
|
|
3949
3898
|
|
|
3950
|
-
|
|
3899
|
+
- **No Linear Motion:** All spatial movement (X, Y, Scale) must use \`spring\`.
|
|
3900
|
+
- *Standard Spring:* \`{ mass: 1, damping: 15, stiffness: 120 }\` (Snappy but smooth)
|
|
3901
|
+
- *Heavy Spring (Mockups):* \`{ mass: 2, damping: 20, stiffness: 100 }\` (Feels weighty)
|
|
3902
|
+
- **Continuous Flow:** Use \`interpolate(frame)\` to ensure \`scale\` or \`rotation\` is changing slightly on EVERY frame. No static resting states.
|
|
3951
3903
|
|
|
3952
|
-
|
|
3953
|
-
- **Implementation:** Staggered entrances with 3-5 frames between items
|
|
3904
|
+
### UI Component Behavior
|
|
3954
3905
|
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
<SlideUp key={item.id} delay={i * 4}>
|
|
3964
|
-
<ListItem data={item} />
|
|
3965
|
-
</SlideUp>
|
|
3966
|
-
))}
|
|
3967
|
-
\`\`\`
|
|
3906
|
+
- **Entrance:** When a UI card enters, it must use **2.5D Rotation**.
|
|
3907
|
+
- *Code:* \`transform: perspective(1000px) rotateX(\${rotateX}deg) translateY(\${y}px)\`
|
|
3908
|
+
- *Values:* Start at \`rotateX(20deg)\` and \`y(100px)\`. Spring to \`0\`.
|
|
3909
|
+
- **Internal Staggering:** If the UI card contains a list or grid, use \`<Sequence>\` or \`delay\` to reveal items one by one (3 frame gap).
|
|
3910
|
+
- **Cursor Interaction:** If a cursor clicks a button:
|
|
3911
|
+
1. Cursor scales \`1 -> 0.8\`
|
|
3912
|
+
2. Button scales \`1 -> 0.95\`
|
|
3913
|
+
3. Release both to \`1\` with a spring
|
|
3968
3914
|
|
|
3969
|
-
|
|
3915
|
+
### Typography Patterns
|
|
3970
3916
|
|
|
3971
|
-
- **
|
|
3972
|
-
- **
|
|
3917
|
+
- **Pattern A (Headline):** "Masked Slide Up". Text translates Y from \`100%\` inside a clipped container.
|
|
3918
|
+
- **Pattern B (Keywords):** "Scale & Glow". Keywords scale to \`1.1\` and emit a \`text-shadow\`.
|
|
3919
|
+
- **Colors:** Text is never pure white (\`#FFF\`). Use \`#F0F0F0\` with a subtle gradient or shadow to add depth.
|
|
3973
3920
|
|
|
3974
|
-
|
|
3975
|
-
const CinematicContainer = ({ children }) => {
|
|
3976
|
-
const frame = useCurrentFrame();
|
|
3977
|
-
const { durationInFrames } = useVideoConfig();
|
|
3978
|
-
const scale = interpolate(frame, [0, durationInFrames], [1, 1.03]);
|
|
3921
|
+
### Background "Aliveness"
|
|
3979
3922
|
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
</AbsoluteFill>
|
|
3984
|
-
);
|
|
3985
|
-
};
|
|
3986
|
-
\`\`\`
|
|
3923
|
+
- Create a \`<MovingBackground>\` component.
|
|
3924
|
+
- It must feature 2-3 \`AbsoluteFill\` gradient orbs that move in a continuous loop using \`Math.sin(frame / slowFactor)\`.
|
|
3925
|
+
- This ensures the video has "depth" behind the text.
|
|
3987
3926
|
|
|
3988
|
-
|
|
3927
|
+
---
|
|
3989
3928
|
|
|
3990
|
-
|
|
3991
|
-
- **Implementation:** Use curved/Bezier paths for cursor movement
|
|
3929
|
+
## 8. Seamless Scene Transitions (No Hard Cuts)
|
|
3992
3930
|
|
|
3993
|
-
|
|
3931
|
+
In high-end motion design, transitions are rarely separate "effects" (like a cross-dissolve). The *outgoing* scene physically pushes, slides, or zooms into the *incoming* scene.
|
|
3994
3932
|
|
|
3995
|
-
|
|
3996
|
-
2. **Layout:** Use \`AbsoluteFill\` for scene composition
|
|
3997
|
-
3. **State:** NO \`useState\` or \`useEffect\` - derive from \`useCurrentFrame()\`
|
|
3933
|
+
### The "Wipe" Rule (No Hard Cuts)
|
|
3998
3934
|
|
|
3999
|
-
|
|
3935
|
+
- **Forbidden:** Never let Scene A end at frame X and Scene B start at frame X+1 with a simple cut.
|
|
3936
|
+
- **Required:** Scene B must exist *above* Scene A (z-index) for at least 15-20 frames before the cut happens.
|
|
3937
|
+
- **Mechanism:** Scene B enters using a "Wipe" motion (sliding in from the bottom/right) or a "Scale In" (expanding from a dot).
|
|
4000
3938
|
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
4. Add subtle camera movement
|
|
4005
|
-
5. Human touches for interactions
|
|
4006
|
-
`;
|
|
4007
|
-
}
|
|
4008
|
-
function generateMicroInteractionsRule() {
|
|
4009
|
-
return `---
|
|
4010
|
-
name: micro-interactions
|
|
4011
|
-
description: Animation components and patterns
|
|
4012
|
-
---
|
|
3939
|
+
### The "Zoom-Through" Technique (Best for SaaS)
|
|
3940
|
+
|
|
3941
|
+
As Scene A ends, the camera must **accelerate** its zoom:
|
|
4013
3942
|
|
|
4014
|
-
|
|
3943
|
+
\`\`\`tsx
|
|
3944
|
+
// Scene A Action: At duration - 20 frames, zoom way in
|
|
3945
|
+
const scaleA = interpolate(frame, [duration - 20, duration], [1.0, 5.0]);
|
|
3946
|
+
|
|
3947
|
+
// Scene B Action: Starts at scale 0.5 and springs to 1.0
|
|
3948
|
+
const scaleB = spring({ frame, fps, config: { damping: 14, stiffness: 120 } });
|
|
3949
|
+
const scaleBValue = interpolate(scaleB, [0, 1], [0.5, 1]);
|
|
3950
|
+
\`\`\`
|
|
4015
3951
|
|
|
4016
|
-
|
|
3952
|
+
Result: It looks like the camera flew *through* Scene A to find Scene B behind it.
|
|
4017
3953
|
|
|
4018
|
-
|
|
4019
|
-
2. **Purposeful** - Every animation communicates something
|
|
4020
|
-
3. **Physics-based** - Use \`spring()\`, not linear easing
|
|
4021
|
-
4. **Continuous** - Always have something moving subtly
|
|
3954
|
+
### The "Color Swipe" (Safety Net)
|
|
4022
3955
|
|
|
4023
|
-
|
|
3956
|
+
If a complex match-cut is too hard, use a "Curtain":
|
|
4024
3957
|
|
|
4025
3958
|
\`\`\`tsx
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
bouncy: { damping: 8, stiffness: 150, mass: 0.8 },
|
|
4030
|
-
gentle: { damping: 30, stiffness: 50, mass: 1 },
|
|
4031
|
-
};
|
|
3959
|
+
// A solid colored AbsoluteFill slides in from left, covers screen for 2 frames,
|
|
3960
|
+
// then slides out to the right, revealing Scene B
|
|
3961
|
+
const curtainX = interpolate(frame, [0, 10, 12, 22], [-100, 0, 0, 100]);
|
|
4032
3962
|
\`\`\`
|
|
4033
3963
|
|
|
4034
|
-
|
|
3964
|
+
### TransitionWrapper Component
|
|
4035
3965
|
|
|
4036
|
-
|
|
3966
|
+
Use the \`TransitionWrapper\` from shared components:
|
|
4037
3967
|
|
|
4038
3968
|
\`\`\`tsx
|
|
4039
|
-
|
|
4040
|
-
const frame = useCurrentFrame();
|
|
4041
|
-
const { fps } = useVideoConfig();
|
|
3969
|
+
import { TransitionWrapper } from './shared';
|
|
4042
3970
|
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
fps,
|
|
4046
|
-
config: { damping: 20, stiffness: 100 }
|
|
4047
|
-
});
|
|
3971
|
+
// Types: 'slide' | 'zoom' | 'fade' | 'none'
|
|
3972
|
+
// enterFrom: 'left' | 'right' | 'bottom'
|
|
4048
3973
|
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
3974
|
+
<TransitionWrapper type="slide" enterFrom="bottom">
|
|
3975
|
+
<YourScene />
|
|
3976
|
+
</TransitionWrapper>
|
|
3977
|
+
\`\`\`
|
|
3978
|
+
|
|
3979
|
+
### Critical: Overlap Your Sequences
|
|
4055
3980
|
|
|
4056
|
-
|
|
3981
|
+
**This is the most important part.** To make transitions work, you must overlap sequences slightly. If Scene 1 is 90 frames, start Scene 2 at frame 75 (15-frame overlap).
|
|
4057
3982
|
|
|
3983
|
+
\`\`\`tsx
|
|
3984
|
+
export const MyVideo = () => {
|
|
4058
3985
|
return (
|
|
4059
|
-
<
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
3986
|
+
<AbsoluteFill>
|
|
3987
|
+
{/* SCENE 1: Ends at frame 100 */}
|
|
3988
|
+
<Sequence from={0} durationInFrames={100}>
|
|
3989
|
+
<SceneOne />
|
|
3990
|
+
</Sequence>
|
|
3991
|
+
|
|
3992
|
+
{/* SCENE 2: Starts at frame 85 (15 frames early!) */}
|
|
3993
|
+
{/* TransitionWrapper slides it ON TOP of Scene 1 */}
|
|
3994
|
+
<Sequence from={85} durationInFrames={150}>
|
|
3995
|
+
<TransitionWrapper type="slide" enterFrom="bottom">
|
|
3996
|
+
<SceneTwo />
|
|
3997
|
+
</TransitionWrapper>
|
|
3998
|
+
</Sequence>
|
|
3999
|
+
|
|
4000
|
+
{/* SCENE 3: Starts early again */}
|
|
4001
|
+
<Sequence from={220} durationInFrames={150}>
|
|
4002
|
+
<TransitionWrapper type="zoom">
|
|
4003
|
+
<SceneThree />
|
|
4004
|
+
</TransitionWrapper>
|
|
4005
|
+
</Sequence>
|
|
4006
|
+
</AbsoluteFill>
|
|
4065
4007
|
);
|
|
4066
4008
|
};
|
|
4067
4009
|
\`\`\`
|
|
4068
4010
|
|
|
4069
|
-
|
|
4011
|
+
**Why this fixes the "Light Switch" effect:** Because Scene 2 physically slides *over* Scene 1 while Scene 1 is still visible underneath, your brain registers it as continuous movement rather than a "cut."
|
|
4070
4012
|
|
|
4071
|
-
|
|
4072
|
-
const StaggeredList = ({ children, itemDelay = 5 }) => (
|
|
4073
|
-
<>
|
|
4074
|
-
{React.Children.map(children, (child, i) => (
|
|
4075
|
-
<AnimatedEntry delay={i * itemDelay}>{child}</AnimatedEntry>
|
|
4076
|
-
))}
|
|
4077
|
-
</>
|
|
4078
|
-
);
|
|
4079
|
-
\`\`\`
|
|
4080
|
-
|
|
4081
|
-
## Interaction Simulation
|
|
4013
|
+
---
|
|
4082
4014
|
|
|
4083
|
-
|
|
4015
|
+
## Quick Tips from Motion Devs
|
|
4084
4016
|
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
const { fps } = useVideoConfig();
|
|
4017
|
+
- **Don't use white backgrounds.** The 2.5D lighting and shadows pop much better on dark grey or black backgrounds.
|
|
4018
|
+
- **Overlap delays.** If Title starts at frame 10 and Card starts at frame 30, the title hasn't finished moving when the card appears. This overlap is crucial for fluidity.
|
|
4019
|
+
- **High FPS.** Ensure your \`remotion.config.ts\` is set to 60fps for springs to look smooth.
|
|
4089
4020
|
|
|
4090
|
-
|
|
4091
|
-
const isReleasing = frame >= pressFrame + 3;
|
|
4021
|
+
---
|
|
4092
4022
|
|
|
4093
|
-
|
|
4094
|
-
frame: frame - pressFrame - 3,
|
|
4095
|
-
fps,
|
|
4096
|
-
config: { damping: 10, stiffness: 300 }
|
|
4097
|
-
}) : 0;
|
|
4023
|
+
## Self-Check: Is It Kinetic?
|
|
4098
4024
|
|
|
4099
|
-
|
|
4025
|
+
Before rendering, verify:
|
|
4100
4026
|
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4027
|
+
- [ ] Camera rig wraps entire scene with drift/zoom
|
|
4028
|
+
- [ ] Every UI element uses 2.5D rotation entrance
|
|
4029
|
+
- [ ] Cursor moves in curves with overshoot
|
|
4030
|
+
- [ ] Lists/grids stagger (never appear all at once)
|
|
4031
|
+
- [ ] Text uses masked reveal or keyword animation
|
|
4032
|
+
- [ ] Background has moving orbs + vignette + noise
|
|
4033
|
+
- [ ] Something is moving on EVERY frame
|
|
4034
|
+
- [ ] No static resting states longer than 30 frames
|
|
4035
|
+
- [ ] Scene transitions overlap (no hard cuts between scenes)
|
|
4036
|
+
- [ ] TransitionWrapper used for slide/zoom entrances
|
|
4104
4037
|
|
|
4105
|
-
|
|
4038
|
+
If your video looks like PowerPoint slides with voiceover, **START OVER**.
|
|
4039
|
+
`
|
|
4040
|
+
},
|
|
4041
|
+
{
|
|
4042
|
+
filename: "failures.md",
|
|
4043
|
+
content: `# Common Failures - READ THIS FIRST
|
|
4106
4044
|
|
|
4107
|
-
|
|
4108
|
-
const TypedText = ({ text, startFrame = 0, speed = 2 }) => {
|
|
4109
|
-
const frame = useCurrentFrame();
|
|
4110
|
-
const charsToShow = Math.floor((frame - startFrame) / speed);
|
|
4045
|
+
If your video looks like any of these, START OVER.
|
|
4111
4046
|
|
|
4112
|
-
|
|
4047
|
+
## FAILURE 1: Slideshow
|
|
4113
4048
|
|
|
4114
|
-
return (
|
|
4115
|
-
<span>
|
|
4116
|
-
{text.slice(0, Math.min(charsToShow, text.length))}
|
|
4117
|
-
{charsToShow < text.length && (
|
|
4118
|
-
<span style={{ opacity: frame % 15 < 8 ? 1 : 0 }}>|</span>
|
|
4119
|
-
)}
|
|
4120
|
-
</span>
|
|
4121
|
-
);
|
|
4122
|
-
};
|
|
4123
4049
|
\`\`\`
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
});
|
|
4133
|
-
const eased = 1 - Math.pow(1 - progress, 3);
|
|
4134
|
-
return <span>{Math.round(from + (to - from) * eased)}</span>;
|
|
4135
|
-
};
|
|
4050
|
+
What you made:
|
|
4051
|
+
- Background image
|
|
4052
|
+
- Text appears on top
|
|
4053
|
+
- Text disappears
|
|
4054
|
+
- New background image
|
|
4055
|
+
- More text
|
|
4056
|
+
|
|
4057
|
+
This is PowerPoint, not a video. REJECTED.
|
|
4136
4058
|
\`\`\`
|
|
4137
4059
|
|
|
4138
|
-
##
|
|
4060
|
+
## FAILURE 2: Lorem Ipsum / Placeholder Content
|
|
4139
4061
|
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
| Stagger delay | 3-8 frames |
|
|
4146
|
-
`;
|
|
4147
|
-
}
|
|
4148
|
-
function generateAnimationComponents() {
|
|
4149
|
-
return `/**
|
|
4150
|
-
* Remotion Animation Components
|
|
4151
|
-
* Copy these into your project as needed.
|
|
4152
|
-
*/
|
|
4153
|
-
|
|
4154
|
-
import React from 'react';
|
|
4155
|
-
import { useCurrentFrame, useVideoConfig, interpolate, spring, Easing } from 'remotion';
|
|
4156
|
-
|
|
4157
|
-
// Spring configurations
|
|
4158
|
-
export const SPRING_CONFIGS = {
|
|
4159
|
-
snappy: { damping: 15, stiffness: 200, mass: 0.5 },
|
|
4160
|
-
smooth: { damping: 20, stiffness: 100, mass: 1 },
|
|
4161
|
-
bouncy: { damping: 8, stiffness: 150, mass: 0.8 },
|
|
4162
|
-
gentle: { damping: 30, stiffness: 50, mass: 1 },
|
|
4163
|
-
};
|
|
4062
|
+
\`\`\`
|
|
4063
|
+
What you made:
|
|
4064
|
+
- "Lorem ipsum dolor sit amet..."
|
|
4065
|
+
- "Sample text here"
|
|
4066
|
+
- Generic placeholder content
|
|
4164
4067
|
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
children: React.ReactNode;
|
|
4168
|
-
delay?: number;
|
|
4169
|
-
direction?: 'up' | 'down' | 'left' | 'right' | 'none';
|
|
4170
|
-
distance?: number;
|
|
4171
|
-
}> = ({ children, delay = 0, direction = 'up', distance = 30 }) => {
|
|
4172
|
-
const frame = useCurrentFrame();
|
|
4173
|
-
const { fps } = useVideoConfig();
|
|
4068
|
+
Use REAL content from the project. REJECTED.
|
|
4069
|
+
\`\`\`
|
|
4174
4070
|
|
|
4175
|
-
|
|
4176
|
-
frame: frame - delay,
|
|
4177
|
-
fps,
|
|
4178
|
-
config: SPRING_CONFIGS.smooth,
|
|
4179
|
-
});
|
|
4071
|
+
## FAILURE 3: Static UI Screenshot
|
|
4180
4072
|
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
none: { x: 0, y: 0 },
|
|
4187
|
-
};
|
|
4073
|
+
\`\`\`
|
|
4074
|
+
What you made:
|
|
4075
|
+
- Screenshot of the app
|
|
4076
|
+
- Text overlay saying "Our Dashboard"
|
|
4077
|
+
- No motion, no interaction
|
|
4188
4078
|
|
|
4189
|
-
|
|
4079
|
+
Recreate the UI in code and ANIMATE it. REJECTED.
|
|
4080
|
+
\`\`\`
|
|
4190
4081
|
|
|
4191
|
-
|
|
4192
|
-
<div style={{
|
|
4193
|
-
opacity: interpolate(progress, [0, 1], [0, 1]),
|
|
4194
|
-
transform: \`translate(\${x * (1 - progress)}px, \${y * (1 - progress)}px)\`,
|
|
4195
|
-
}}>
|
|
4196
|
-
{children}
|
|
4197
|
-
</div>
|
|
4198
|
-
);
|
|
4199
|
-
};
|
|
4082
|
+
## FAILURE 4: Elements Just Appearing
|
|
4200
4083
|
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
}> = ({ children, delay = 0, from = 0.8 }) => {
|
|
4207
|
-
const frame = useCurrentFrame();
|
|
4208
|
-
const { fps } = useVideoConfig();
|
|
4084
|
+
\`\`\`
|
|
4085
|
+
What you made:
|
|
4086
|
+
- Frame 0: nothing
|
|
4087
|
+
- Frame 1: element is fully visible
|
|
4088
|
+
- No transition, no animation
|
|
4209
4089
|
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
fps,
|
|
4213
|
-
config: SPRING_CONFIGS.bouncy,
|
|
4214
|
-
});
|
|
4090
|
+
Every element must animate in with spring/easing. REJECTED.
|
|
4091
|
+
\`\`\`
|
|
4215
4092
|
|
|
4216
|
-
|
|
4217
|
-
<div style={{
|
|
4218
|
-
opacity: interpolate(progress, [0, 0.5], [0, 1], { extrapolateRight: 'clamp' }),
|
|
4219
|
-
transform: \`scale(\${interpolate(progress, [0, 1], [from, 1])})\`,
|
|
4220
|
-
}}>
|
|
4221
|
-
{children}
|
|
4222
|
-
</div>
|
|
4223
|
-
);
|
|
4224
|
-
};
|
|
4093
|
+
## FAILURE 5: Hard Cuts Between Scenes (The "Light Switch" Effect)
|
|
4225
4094
|
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
);
|
|
4095
|
+
\`\`\`
|
|
4096
|
+
What you made:
|
|
4097
|
+
- Scene 1 ends at frame 100
|
|
4098
|
+
- Scene 2 starts at frame 101
|
|
4099
|
+
- No overlap, no slide, no zoom-through
|
|
4100
|
+
- Feels like a light switching on/off
|
|
4101
|
+
|
|
4102
|
+
FIX: Overlap sequences by 15-20 frames. Scene 2 must slide/zoom
|
|
4103
|
+
ON TOP of Scene 1 while Scene 1 is still visible. Use TransitionWrapper.
|
|
4104
|
+
REJECTED.
|
|
4105
|
+
\`\`\`
|
|
4238
4106
|
|
|
4239
|
-
|
|
4240
|
-
export const ButtonPress: React.FC<{
|
|
4241
|
-
children: React.ReactNode;
|
|
4242
|
-
pressFrame: number;
|
|
4243
|
-
}> = ({ children, pressFrame }) => {
|
|
4244
|
-
const frame = useCurrentFrame();
|
|
4245
|
-
const { fps } = useVideoConfig();
|
|
4107
|
+
## FAILURE 6: Static Camera
|
|
4246
4108
|
|
|
4247
|
-
|
|
4248
|
-
|
|
4109
|
+
\`\`\`
|
|
4110
|
+
What you made:
|
|
4111
|
+
- Camera never moves
|
|
4112
|
+
- No drift, no zoom, no rotation
|
|
4113
|
+
- Feels like watching a screenshot
|
|
4249
4114
|
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
fps,
|
|
4253
|
-
config: { damping: 10, stiffness: 300 },
|
|
4254
|
-
}) : 0;
|
|
4115
|
+
Camera must ALWAYS be subtly moving. REJECTED.
|
|
4116
|
+
\`\`\`
|
|
4255
4117
|
|
|
4256
|
-
|
|
4118
|
+
## FAILURE 7: Linear Cursor Movement
|
|
4257
4119
|
|
|
4258
|
-
|
|
4259
|
-
|
|
4120
|
+
\`\`\`
|
|
4121
|
+
What you made:
|
|
4122
|
+
- Cursor moves in straight lines
|
|
4123
|
+
- No overshoot on stops
|
|
4124
|
+
- Click has no feedback
|
|
4260
4125
|
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
const frame = useCurrentFrame();
|
|
4269
|
-
const charsToShow = Math.floor((frame - startFrame) / speed);
|
|
4126
|
+
Cursor must move in Bezier curves with overshoot. REJECTED.
|
|
4127
|
+
\`\`\`
|
|
4128
|
+
`
|
|
4129
|
+
},
|
|
4130
|
+
{
|
|
4131
|
+
filename: "parameterization.md",
|
|
4132
|
+
content: `# Parameterization (Critical)
|
|
4270
4133
|
|
|
4271
|
-
|
|
4134
|
+
Never hardcode frame numbers. Use variables for all keyframes.
|
|
4272
4135
|
|
|
4273
|
-
|
|
4136
|
+
## Why
|
|
4274
4137
|
|
|
4275
|
-
|
|
4276
|
-
<span>
|
|
4277
|
-
{text.slice(0, Math.min(charsToShow, text.length))}
|
|
4278
|
-
{showCursor && isTyping && (
|
|
4279
|
-
<span style={{ opacity: frame % 15 < 8 ? 1 : 0 }}>|</span>
|
|
4280
|
-
)}
|
|
4281
|
-
</span>
|
|
4282
|
-
);
|
|
4283
|
-
};
|
|
4138
|
+
When you change timing early in video, everything else breaks if hardcoded.
|
|
4284
4139
|
|
|
4285
|
-
|
|
4286
|
-
export const CountingNumber: React.FC<{
|
|
4287
|
-
from?: number;
|
|
4288
|
-
to: number;
|
|
4289
|
-
startFrame?: number;
|
|
4290
|
-
duration?: number;
|
|
4291
|
-
format?: (n: number) => string;
|
|
4292
|
-
}> = ({ from = 0, to, startFrame = 0, duration = 30, format = String }) => {
|
|
4293
|
-
const frame = useCurrentFrame();
|
|
4140
|
+
## Pattern
|
|
4294
4141
|
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4142
|
+
\`\`\`ts
|
|
4143
|
+
// Define keyframes as variables
|
|
4144
|
+
const SCENE_START = 0;
|
|
4145
|
+
const TITLE_IN = SCENE_START + 15;
|
|
4146
|
+
const TITLE_HOLD = TITLE_IN + 60;
|
|
4147
|
+
const TITLE_OUT = TITLE_HOLD + 15;
|
|
4148
|
+
const SUBTITLE_IN = TITLE_IN + 20; // Relative to title
|
|
4149
|
+
const SCENE_END = TITLE_OUT + 30;
|
|
4299
4150
|
|
|
4300
|
-
|
|
4301
|
-
|
|
4151
|
+
// Use in animations
|
|
4152
|
+
opacity: interpolate(frame, [TITLE_IN, TITLE_IN + 15], [0, 1])
|
|
4153
|
+
\`\`\`
|
|
4302
4154
|
|
|
4303
|
-
|
|
4304
|
-
};
|
|
4155
|
+
## Scene Duration
|
|
4305
4156
|
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
speed?: number;
|
|
4311
|
-
}> = ({ children, amplitude = 3, speed = 0.05 }) => {
|
|
4312
|
-
const frame = useCurrentFrame();
|
|
4313
|
-
const y = Math.sin(frame * speed) * amplitude;
|
|
4157
|
+
\`\`\`ts
|
|
4158
|
+
// Calculate from keyframes, don't hardcode
|
|
4159
|
+
const sceneDuration = SCENE_END - SCENE_START;
|
|
4160
|
+
\`\`\`
|
|
4314
4161
|
|
|
4315
|
-
|
|
4316
|
-
};
|
|
4162
|
+
## Audio Sync
|
|
4317
4163
|
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
}> = ({ children, startFrame, duration = 45 }) => {
|
|
4324
|
-
const frame = useCurrentFrame();
|
|
4325
|
-
const { fps } = useVideoConfig();
|
|
4164
|
+
\`\`\`ts
|
|
4165
|
+
// If audio timestamp changes, only update one variable
|
|
4166
|
+
const VOICEOVER_START = 45;
|
|
4167
|
+
const VISUAL_CUE = VOICEOVER_START + 10; // Auto-adjusts
|
|
4168
|
+
\`\`\`
|
|
4326
4169
|
|
|
4327
|
-
|
|
4328
|
-
const progress = spring({
|
|
4329
|
-
frame: isActive ? frame - startFrame : 0,
|
|
4330
|
-
fps,
|
|
4331
|
-
config: SPRING_CONFIGS.snappy,
|
|
4332
|
-
});
|
|
4170
|
+
## Multi-Scene
|
|
4333
4171
|
|
|
4334
|
-
|
|
4172
|
+
\`\`\`ts
|
|
4173
|
+
const SCENE_1_START = 0;
|
|
4174
|
+
const SCENE_1_END = SCENE_1_START + 90;
|
|
4175
|
+
const SCENE_2_START = SCENE_1_END - 15; // 15 frame overlap for transition
|
|
4176
|
+
const SCENE_2_END = SCENE_2_START + 120;
|
|
4177
|
+
\`\`\`
|
|
4178
|
+
`
|
|
4179
|
+
},
|
|
4180
|
+
{
|
|
4181
|
+
filename: "layers.md",
|
|
4182
|
+
content: `# Layers & Composition
|
|
4335
4183
|
|
|
4336
|
-
|
|
4337
|
-
<div style={{
|
|
4338
|
-
transform: \`scale(\${scale})\`,
|
|
4339
|
-
boxShadow: isActive ? \`0 \${8 + progress * 12}px \${16 + progress * 24}px rgba(0,0,0,0.15)\` : undefined,
|
|
4340
|
-
}}>
|
|
4341
|
-
{children}
|
|
4342
|
-
</div>
|
|
4343
|
-
);
|
|
4344
|
-
};
|
|
4184
|
+
Explicit z-index prevents visual bugs.
|
|
4345
4185
|
|
|
4346
|
-
|
|
4347
|
-
export const CursorPointer: React.FC<{
|
|
4348
|
-
path: Array<{ x: number; y: number; frame: number }>;
|
|
4349
|
-
size?: number;
|
|
4350
|
-
}> = ({ path, size = 24 }) => {
|
|
4351
|
-
const frame = useCurrentFrame();
|
|
4352
|
-
const { fps } = useVideoConfig();
|
|
4186
|
+
## Z-Index Scale
|
|
4353
4187
|
|
|
4354
|
-
|
|
4355
|
-
|
|
4188
|
+
| Layer | Z-Index | Examples |
|
|
4189
|
+
|-------|---------|----------|
|
|
4190
|
+
| Background orbs | 0 | Moving gradients, noise |
|
|
4191
|
+
| Vignette | 1 | Dark corners overlay |
|
|
4192
|
+
| UI Base | 10 | Main UI container |
|
|
4193
|
+
| UI Elements | 20 | Buttons, cards, inputs |
|
|
4194
|
+
| Overlays | 30 | Modals, tooltips, highlights |
|
|
4195
|
+
| Text/Captions | 40 | Titles, labels |
|
|
4196
|
+
| Cursor | 50 | Mouse pointer |
|
|
4197
|
+
| Debug | 100 | Frame counter (remove in final) |
|
|
4356
4198
|
|
|
4357
|
-
|
|
4358
|
-
const from = path[i];
|
|
4359
|
-
const to = path[i + 1];
|
|
4199
|
+
## Composition Structure
|
|
4360
4200
|
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4201
|
+
\`\`\`tsx
|
|
4202
|
+
<AbsoluteFill>
|
|
4203
|
+
{/* Background layer */}
|
|
4204
|
+
<MovingBackground />
|
|
4205
|
+
|
|
4206
|
+
{/* Vignette */}
|
|
4207
|
+
<Vignette />
|
|
4208
|
+
|
|
4209
|
+
{/* Camera rig wraps all content */}
|
|
4210
|
+
<CameraRig>
|
|
4211
|
+
<Sequence from={0}>
|
|
4212
|
+
<Scene1 />
|
|
4213
|
+
</Sequence>
|
|
4214
|
+
<Sequence from={SCENE_2_START}>
|
|
4215
|
+
<Scene2 />
|
|
4216
|
+
</Sequence>
|
|
4217
|
+
</CameraRig>
|
|
4218
|
+
|
|
4219
|
+
{/* Audio */}
|
|
4220
|
+
<Audio src={music} volume={0.3} />
|
|
4221
|
+
<Audio src={voiceover} />
|
|
4222
|
+
</AbsoluteFill>
|
|
4223
|
+
\`\`\`
|
|
4367
4224
|
|
|
4368
|
-
|
|
4369
|
-
y = interpolate(progress, [0, 1], [from.y, to.y]);
|
|
4370
|
-
break;
|
|
4371
|
-
} else if (frame > to.frame) {
|
|
4372
|
-
x = to.x;
|
|
4373
|
-
y = to.y;
|
|
4374
|
-
}
|
|
4375
|
-
}
|
|
4225
|
+
## Overlapping Sequences
|
|
4376
4226
|
|
|
4377
|
-
|
|
4378
|
-
<div style={{
|
|
4379
|
-
position: 'absolute',
|
|
4380
|
-
left: \`\${x}%\`,
|
|
4381
|
-
top: \`\${y}%\`,
|
|
4382
|
-
transform: 'translate(-50%, -50%)',
|
|
4383
|
-
zIndex: 1000,
|
|
4384
|
-
pointerEvents: 'none',
|
|
4385
|
-
}}>
|
|
4386
|
-
<svg width={size} height={size} viewBox="0 0 24 24">
|
|
4387
|
-
<path
|
|
4388
|
-
d="M4 4 L4 20 L9 15 L13 22 L16 20 L12 13 L19 13 Z"
|
|
4389
|
-
fill="white"
|
|
4390
|
-
stroke="black"
|
|
4391
|
-
strokeWidth="1.5"
|
|
4392
|
-
/>
|
|
4393
|
-
</svg>
|
|
4394
|
-
</div>
|
|
4395
|
-
);
|
|
4396
|
-
};
|
|
4397
|
-
`;
|
|
4398
|
-
}
|
|
4399
|
-
function generateComponentIntegrationRule(b) {
|
|
4400
|
-
const cmd2 = b.name;
|
|
4401
|
-
return `---
|
|
4402
|
-
name: component-integration
|
|
4403
|
-
description: Integrating app components into Remotion videos
|
|
4404
|
-
---
|
|
4227
|
+
For zoom-through transitions, scenes overlap:
|
|
4405
4228
|
|
|
4406
|
-
|
|
4229
|
+
\`\`\`tsx
|
|
4230
|
+
<Sequence from={0} durationInFrames={100}>
|
|
4231
|
+
<Scene1 />
|
|
4232
|
+
</Sequence>
|
|
4233
|
+
<Sequence from={80} durationInFrames={100}> {/* 20 frame overlap */}
|
|
4234
|
+
<Scene2 />
|
|
4235
|
+
</Sequence>
|
|
4236
|
+
\`\`\`
|
|
4237
|
+
`
|
|
4238
|
+
},
|
|
4239
|
+
{
|
|
4240
|
+
filename: "project-based.md",
|
|
4241
|
+
content: `# Component Extraction (Critical for Product Videos)
|
|
4407
4242
|
|
|
4408
|
-
|
|
4243
|
+
When creating a video for a project, **copy the actual components** - don't rebuild from scratch.
|
|
4409
4244
|
|
|
4410
|
-
##
|
|
4245
|
+
## The Goal
|
|
4411
4246
|
|
|
4412
|
-
|
|
4413
|
-
Read your app's components, note every visual detail, build identical-looking components in Remotion.
|
|
4247
|
+
Your video should show the REAL product UI, pixel-perfect. Viewers should not be able to tell the difference between a screenshot and your video (except yours is animated with kinetic motion).
|
|
4414
4248
|
|
|
4415
|
-
|
|
4249
|
+
## Process: Eject \u2192 Simplify \u2192 Animate
|
|
4416
4250
|
|
|
4417
|
-
###
|
|
4418
|
-
For truly simple presentational components, you can copy them directly.
|
|
4251
|
+
### Step 1: Find Components to Feature
|
|
4419
4252
|
|
|
4420
4253
|
\`\`\`bash
|
|
4421
|
-
|
|
4422
|
-
|
|
4254
|
+
# Explore the project structure
|
|
4255
|
+
ls src/components/
|
|
4256
|
+
ls src/app/
|
|
4257
|
+
# Find the key UI: dashboards, forms, cards, modals, etc.
|
|
4423
4258
|
\`\`\`
|
|
4424
4259
|
|
|
4425
|
-
|
|
4260
|
+
### Step 2: Copy Component Code
|
|
4426
4261
|
|
|
4427
|
-
|
|
4262
|
+
Copy the actual component file into your Remotion project.
|
|
4428
4263
|
|
|
4429
|
-
###
|
|
4264
|
+
### Step 3: Eject - Strip Logic, Keep Visuals
|
|
4430
4265
|
|
|
4431
4266
|
\`\`\`tsx
|
|
4432
|
-
// BEFORE
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
<
|
|
4437
|
-
|
|
4267
|
+
// BEFORE: Original component from project
|
|
4268
|
+
export function PricingCard({ plan, onSelect }) {
|
|
4269
|
+
const [loading, setLoading] = useState(false);
|
|
4270
|
+
const handleClick = async () => { /* API calls */ };
|
|
4271
|
+
return <div>...</div>;
|
|
4272
|
+
}
|
|
4438
4273
|
|
|
4439
|
-
|
|
4274
|
+
// AFTER: Ejected for Remotion
|
|
4275
|
+
export function PricingCard({ plan, progress, isHovered }) {
|
|
4276
|
+
// No useState, no API calls, no handlers
|
|
4277
|
+
// Just visual + animation props from Remotion
|
|
4440
4278
|
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
const { data } = useQuery('GET_USERS');
|
|
4279
|
+
const entranceProgress = spring({ frame: progress, fps, config: { mass: 2, damping: 20, stiffness: 100 } });
|
|
4280
|
+
const rotateX = interpolate(entranceProgress, [0, 1], [20, 0]);
|
|
4444
4281
|
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4282
|
+
return (
|
|
4283
|
+
<div style={{
|
|
4284
|
+
transform: \`perspective(1000px) rotateX(\${rotateX}deg)\`,
|
|
4285
|
+
// ... exact same visual styles as original
|
|
4286
|
+
}}>
|
|
4287
|
+
...
|
|
4288
|
+
</div>
|
|
4289
|
+
);
|
|
4290
|
+
}
|
|
4450
4291
|
\`\`\`
|
|
4451
4292
|
|
|
4452
|
-
###
|
|
4293
|
+
### Step 4: Add Kinetic Animation
|
|
4453
4294
|
|
|
4454
|
-
|
|
4455
|
-
|
|
4295
|
+
Apply the rules from kinetic-saas.md:
|
|
4296
|
+
- 2.5D rotation entrance
|
|
4297
|
+
- Spring physics
|
|
4298
|
+
- Staggered children
|
|
4299
|
+
- Cursor interaction states
|
|
4456
4300
|
|
|
4457
|
-
|
|
4458
|
-
<Navbar />
|
|
4459
|
-
</FadeIn>
|
|
4301
|
+
### Step 5: Fill With Real Demo Data
|
|
4460
4302
|
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4303
|
+
\`\`\`tsx
|
|
4304
|
+
// Use realistic data that matches the product
|
|
4305
|
+
const DEMO_PLANS = [
|
|
4306
|
+
{ name: 'Starter', price: 9, features: ['5 projects', '10GB storage'] },
|
|
4307
|
+
{ name: 'Pro', price: 29, features: ['Unlimited projects', '100GB storage', 'Priority support'] },
|
|
4308
|
+
];
|
|
4309
|
+
// NOT: "Plan A", "$XX/mo", "Feature 1"
|
|
4464
4310
|
\`\`\`
|
|
4465
4311
|
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
## Common Showcase Patterns
|
|
4312
|
+
## What to Copy
|
|
4469
4313
|
|
|
4470
|
-
|
|
4314
|
+
| Find in Project | Copy to Video |
|
|
4315
|
+
|-----------------|---------------|
|
|
4316
|
+
| \`tailwind.config.js\` | Colors, fonts, spacing |
|
|
4317
|
+
| \`globals.css\` | CSS variables, font imports |
|
|
4318
|
+
| \`/public/logo.svg\` | Actual logo file |
|
|
4319
|
+
| \`/src/components/*.tsx\` | Component structure & styles |
|
|
4471
4320
|
|
|
4472
|
-
|
|
4473
|
-
const DashboardShowcase = () => {
|
|
4474
|
-
return (
|
|
4475
|
-
<DashboardLayout>
|
|
4476
|
-
<FadeIn delay={0}>
|
|
4477
|
-
<Header user={mockUser} />
|
|
4478
|
-
</FadeIn>
|
|
4479
|
-
|
|
4480
|
-
<div className="grid grid-cols-3 gap-4">
|
|
4481
|
-
<SlideUp delay={15}><StatsWidget data={revenueData} /></SlideUp>
|
|
4482
|
-
<SlideUp delay={23}><StatsWidget data={usersData} /></SlideUp>
|
|
4483
|
-
<SlideUp delay={31}><StatsWidget data={ordersData} /></SlideUp>
|
|
4484
|
-
</div>
|
|
4485
|
-
|
|
4486
|
-
<FadeIn delay={45}>
|
|
4487
|
-
<ChartWidget data={chartData} />
|
|
4488
|
-
</FadeIn>
|
|
4489
|
-
</DashboardLayout>
|
|
4490
|
-
);
|
|
4491
|
-
};
|
|
4492
|
-
\`\`\`
|
|
4321
|
+
## Common Mistakes
|
|
4493
4322
|
|
|
4494
|
-
|
|
4323
|
+
\u274C Using placeholder colors like \`#333\`
|
|
4324
|
+
\u2705 Using exact project colors from tailwind config
|
|
4495
4325
|
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
const frame = useCurrentFrame();
|
|
4499
|
-
const { fps } = useVideoConfig();
|
|
4326
|
+
\u274C Generic content: "Feature 1", "Lorem ipsum"
|
|
4327
|
+
\u2705 Real content: "Unlimited projects", "Priority support"
|
|
4500
4328
|
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
<Input
|
|
4504
|
-
label="Email"
|
|
4505
|
-
value={<TextReveal text="sarah@example.com" startFrame={0} />}
|
|
4506
|
-
/>
|
|
4507
|
-
<Input
|
|
4508
|
-
label="Password"
|
|
4509
|
-
type="password"
|
|
4510
|
-
value={frame > fps * 2 ? '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022' : ''}
|
|
4511
|
-
/>
|
|
4512
|
-
</LoginForm>
|
|
4513
|
-
);
|
|
4514
|
-
};
|
|
4515
|
-
\`\`\`
|
|
4329
|
+
\u274C Building from scratch based on screenshots
|
|
4330
|
+
\u2705 Copying actual component code and ejecting it
|
|
4516
4331
|
|
|
4517
|
-
|
|
4332
|
+
\u274C Static fade-in animations
|
|
4333
|
+
\u2705 2.5D rotation with spring physics (kinetic style)
|
|
4334
|
+
`
|
|
4335
|
+
},
|
|
4336
|
+
{
|
|
4337
|
+
filename: "social-media.md",
|
|
4338
|
+
content: `# Social Media Video Guide (TikTok/Reels/Shorts)
|
|
4518
4339
|
|
|
4519
|
-
|
|
4520
|
-
const ModalShowcase = () => {
|
|
4521
|
-
const frame = useCurrentFrame();
|
|
4522
|
-
const showModal = frame > 30;
|
|
4340
|
+
**Platforms:** TikTok, Instagram Reels, YouTube Shorts (all 9:16 vertical)
|
|
4523
4341
|
|
|
4524
|
-
|
|
4525
|
-
<>
|
|
4526
|
-
<PageBackground />
|
|
4527
|
-
{showModal && (
|
|
4528
|
-
<>
|
|
4529
|
-
<FadeIn delay={30}>
|
|
4530
|
-
<div className="absolute inset-0 bg-black/50" />
|
|
4531
|
-
</FadeIn>
|
|
4532
|
-
<SlideUp delay={35}>
|
|
4533
|
-
<ConfirmationModal title="Confirm Delete" message="Are you sure?" isOpen />
|
|
4534
|
-
</SlideUp>
|
|
4535
|
-
</>
|
|
4536
|
-
)}
|
|
4537
|
-
</>
|
|
4538
|
-
);
|
|
4539
|
-
};
|
|
4540
|
-
\`\`\`
|
|
4342
|
+
**Philosophy:** "Don't make ads. Make TikToks." Your video must look native to the platform - lo-fi beats polished.
|
|
4541
4343
|
|
|
4542
4344
|
---
|
|
4543
4345
|
|
|
4544
|
-
##
|
|
4346
|
+
## 1. The "3-Second War"
|
|
4545
4347
|
|
|
4546
|
-
|
|
4547
|
-
\`\`\`tsx
|
|
4548
|
-
// PROBLEM: useRouter, useAuth won't work
|
|
4549
|
-
// SOLUTION: Pass as props or mock the context
|
|
4550
|
-
const MockAuthProvider = ({ children }) => (
|
|
4551
|
-
<AuthContext.Provider value={{ user: mockUser }}>
|
|
4552
|
-
{children}
|
|
4553
|
-
</AuthContext.Provider>
|
|
4554
|
-
);
|
|
4555
|
-
\`\`\`
|
|
4348
|
+
You have 3 seconds before the thumb scrolls. Win or lose everything.
|
|
4556
4349
|
|
|
4557
|
-
###
|
|
4558
|
-
\`\`\`tsx
|
|
4559
|
-
// Use transform scale to fit
|
|
4560
|
-
<div style={{ transform: 'scale(0.8)', transformOrigin: 'top left' }}>
|
|
4561
|
-
<LargeComponent />
|
|
4562
|
-
</div>
|
|
4563
|
-
\`\`\`
|
|
4564
|
-
`;
|
|
4565
|
-
}
|
|
4566
|
-
function generateProjectVideoWorkflowRule(b) {
|
|
4567
|
-
const cmd2 = b.name;
|
|
4568
|
-
return `---
|
|
4569
|
-
name: project-video-workflow
|
|
4570
|
-
description: Create promotional videos using actual project UI
|
|
4571
|
-
---
|
|
4350
|
+
### Hook Requirements (0-3s)
|
|
4572
4351
|
|
|
4573
|
-
|
|
4352
|
+
- **Text on screen in FRAME 1** - no waiting for audio
|
|
4353
|
+
- **Movement** - never start static
|
|
4354
|
+
- **Pattern interrupt** - something unexpected
|
|
4574
4355
|
|
|
4575
|
-
|
|
4356
|
+
### Hook Types
|
|
4576
4357
|
|
|
4577
|
-
|
|
4358
|
+
| Type | Description | Use For |
|
|
4359
|
+
|------|-------------|---------|
|
|
4360
|
+
| Face Close-Up | Tight on face, zoom out | Personal/story |
|
|
4361
|
+
| Green Screen | Speaker + background tweet/article | Commentary |
|
|
4362
|
+
| Text Slam | Bold text hits screen with sound | Listicles, tips |
|
|
4363
|
+
| Movement | Object thrown, quick zoom | Product demos |
|
|
4578
4364
|
|
|
4579
|
-
|
|
4580
|
-
- User wants "product demo", "feature walkthrough", or "promotional video"
|
|
4581
|
-
- User mentions showcasing specific features/UI
|
|
4582
|
-
- User wants to animate their actual app interface
|
|
4365
|
+
### Audio Hooks
|
|
4583
4366
|
|
|
4584
|
-
|
|
4367
|
+
Bad: "Today I want to talk about..."
|
|
4368
|
+
Good: "Stop scrolling if you hate [X]." / "This feels illegal to know."
|
|
4585
4369
|
|
|
4586
|
-
|
|
4587
|
-
# 1. Scaffold video project
|
|
4588
|
-
${cmd2} video init my-app-promo
|
|
4589
|
-
cd my-app-promo
|
|
4370
|
+
---
|
|
4590
4371
|
|
|
4591
|
-
|
|
4592
|
-
${cmd2} video create \\
|
|
4593
|
-
--script "Introducing our new app..." \\
|
|
4594
|
-
--output ./public
|
|
4372
|
+
## 2. Pacing Rules (No Dead Air)
|
|
4595
4373
|
|
|
4596
|
-
|
|
4374
|
+
| Rule | Implementation |
|
|
4375
|
+
|------|----------------|
|
|
4376
|
+
| Visual change every 2-3s | Cut, zoom, or new element every 60-90 frames |
|
|
4377
|
+
| No "Millennial Pause" | Never start with 1-2s of stillness |
|
|
4378
|
+
| Remove all breaths | Edit out pauses between sentences |
|
|
4379
|
+
| Jump cuts are native | Don't smooth-cut; jump cuts feel authentic |
|
|
4597
4380
|
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4381
|
+
---
|
|
4382
|
+
|
|
4383
|
+
## 3. Native Lo-Fi Aesthetic
|
|
4384
|
+
|
|
4385
|
+
| Looks Like TV Ad (Bad) | Looks Native (Good) |
|
|
4386
|
+
|------------------------|---------------------|
|
|
4387
|
+
| Perfect lighting | Natural/ring light |
|
|
4388
|
+
| Broadcast fonts | Platform-native fonts |
|
|
4389
|
+
| Smooth transitions | Jump cuts |
|
|
4390
|
+
| Color graded | Raw, slightly oversaturated |
|
|
4391
|
+
|
|
4392
|
+
### Native Font Stack
|
|
4393
|
+
|
|
4394
|
+
\`\`\`tsx
|
|
4395
|
+
const NATIVE_FONT = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif";
|
|
4601
4396
|
\`\`\`
|
|
4602
4397
|
|
|
4603
4398
|
---
|
|
4604
4399
|
|
|
4605
|
-
##
|
|
4400
|
+
## 4. Safe Zone Rules (CRITICAL)
|
|
4606
4401
|
|
|
4607
|
-
|
|
4402
|
+
Platform UI overlays your content. Plan for it.
|
|
4608
4403
|
|
|
4609
|
-
\`\`\`
|
|
4610
|
-
|
|
4611
|
-
|
|
4404
|
+
\`\`\`
|
|
4405
|
+
1080x1920 Canvas
|
|
4406
|
+
+------------------------------------------+
|
|
4407
|
+
| TOP SAFE ZONE (150px) | <- Notch, status bar
|
|
4408
|
+
| ----------------------------------------|
|
|
4409
|
+
| |
|
|
4410
|
+
| CONTENT SAFE ZONE |
|
|
4411
|
+
| (60px left, 120px right margins) |
|
|
4412
|
+
| |
|
|
4413
|
+
| ----------------------------------------|
|
|
4414
|
+
| BOTTOM SAFE ZONE (384px / 20%) | <- Captions, username
|
|
4415
|
+
| [Icon strip on right: 120px] | <- Like, Comment, Share
|
|
4416
|
+
+------------------------------------------+
|
|
4417
|
+
\`\`\`
|
|
4418
|
+
|
|
4419
|
+
### Safe Zone Values
|
|
4612
4420
|
|
|
4613
|
-
|
|
4614
|
-
|
|
4421
|
+
\`\`\`tsx
|
|
4422
|
+
const SAFE_ZONE = {
|
|
4423
|
+
top: 150, // Notch/status bar
|
|
4424
|
+
bottom: 384, // 20% of 1920 - captions/username
|
|
4425
|
+
left: 60, // Edge safety
|
|
4426
|
+
right: 120, // Like/Comment/Share icons
|
|
4427
|
+
};
|
|
4615
4428
|
|
|
4616
|
-
|
|
4617
|
-
|
|
4429
|
+
// Captions must sit ABOVE bottom safe zone
|
|
4430
|
+
const CAPTION_BOTTOM = 350; // px from bottom
|
|
4618
4431
|
\`\`\`
|
|
4619
4432
|
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4433
|
+
### SafeZoneGuide Component
|
|
4434
|
+
|
|
4435
|
+
\`\`\`tsx
|
|
4436
|
+
const SafeZoneGuide: React.FC = () => (
|
|
4437
|
+
<AbsoluteFill style={{ pointerEvents: "none" }}>
|
|
4438
|
+
{/* Top danger zone */}
|
|
4439
|
+
<div style={{
|
|
4440
|
+
position: "absolute", top: 0, left: 0, right: 0, height: 150,
|
|
4441
|
+
backgroundColor: "rgba(255,0,0,0.2)", borderBottom: "2px dashed red",
|
|
4442
|
+
}} />
|
|
4443
|
+
{/* Bottom danger zone */}
|
|
4444
|
+
<div style={{
|
|
4445
|
+
position: "absolute", bottom: 0, left: 0, right: 0, height: 384,
|
|
4446
|
+
backgroundColor: "rgba(255,0,0,0.2)", borderTop: "2px dashed red",
|
|
4447
|
+
}} />
|
|
4448
|
+
{/* Right icon strip */}
|
|
4449
|
+
<div style={{
|
|
4450
|
+
position: "absolute", top: 150, right: 0, bottom: 384, width: 120,
|
|
4451
|
+
backgroundColor: "rgba(255,165,0,0.2)", borderLeft: "2px dashed orange",
|
|
4452
|
+
}} />
|
|
4453
|
+
</AbsoluteFill>
|
|
4454
|
+
);
|
|
4455
|
+
\`\`\`
|
|
4625
4456
|
|
|
4626
|
-
|
|
4457
|
+
---
|
|
4627
4458
|
|
|
4628
|
-
|
|
4629
|
-
## Brand: [App Name]
|
|
4459
|
+
## 5. Content Formats
|
|
4630
4460
|
|
|
4631
|
-
###
|
|
4632
|
-
- Background: #0f172a
|
|
4633
|
-
- Surface: #1e293b
|
|
4634
|
-
- Primary: #14b8a6
|
|
4635
|
-
- Text: #ffffff
|
|
4461
|
+
### Green Screen (Speaker + Background)
|
|
4636
4462
|
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4463
|
+
\`\`\`tsx
|
|
4464
|
+
<AbsoluteFill>
|
|
4465
|
+
<Img src={backgroundImage} style={{ width: "100%", height: "100%", objectFit: "cover" }} />
|
|
4466
|
+
<div style={{
|
|
4467
|
+
position: "absolute", bottom: 400, left: 40,
|
|
4468
|
+
width: 300, height: 400, borderRadius: 20, overflow: "hidden",
|
|
4469
|
+
}}>
|
|
4470
|
+
<Video src={speakerVideo} />
|
|
4471
|
+
</div>
|
|
4472
|
+
</AbsoluteFill>
|
|
4641
4473
|
\`\`\`
|
|
4642
4474
|
|
|
4643
|
-
###
|
|
4475
|
+
### Listicle (Rapid-Fire)
|
|
4644
4476
|
|
|
4645
|
-
\`\`\`
|
|
4646
|
-
|
|
4477
|
+
\`\`\`tsx
|
|
4478
|
+
const FRAMES_PER_ITEM = 60; // 2 seconds per item
|
|
4479
|
+
const currentIndex = Math.floor(frame / FRAMES_PER_ITEM);
|
|
4480
|
+
// Slam-in with spring animation for each item
|
|
4481
|
+
\`\`\`
|
|
4647
4482
|
|
|
4648
|
-
###
|
|
4649
|
-
- Logo centered
|
|
4650
|
-
- Tagline fades up
|
|
4483
|
+
### Visual ASMR (No Talking)
|
|
4651
4484
|
|
|
4652
|
-
|
|
4653
|
-
-
|
|
4654
|
-
-
|
|
4485
|
+
- No voiceover, music only
|
|
4486
|
+
- Slow, deliberate cursor movements
|
|
4487
|
+
- Satisfying click feedback
|
|
4488
|
+
- Ken Burns zoom on details
|
|
4655
4489
|
|
|
4656
|
-
###
|
|
4657
|
-
- Sidebar slides in
|
|
4658
|
-
- Selection animates
|
|
4490
|
+
### POV Skit
|
|
4659
4491
|
|
|
4660
|
-
|
|
4661
|
-
|
|
4492
|
+
\`\`\`tsx
|
|
4493
|
+
<div style={{ position: "absolute", top: 160, left: 40, right: 120, fontSize: 42 }}>
|
|
4494
|
+
POV: {setup}
|
|
4495
|
+
</div>
|
|
4662
4496
|
\`\`\`
|
|
4663
4497
|
|
|
4664
|
-
|
|
4498
|
+
---
|
|
4499
|
+
|
|
4500
|
+
## 6. TikTok Captions
|
|
4665
4501
|
|
|
4666
|
-
|
|
4502
|
+
Use \`@remotion/captions\` with \`createTikTokStyleCaptions\`:
|
|
4667
4503
|
|
|
4668
4504
|
\`\`\`tsx
|
|
4669
|
-
|
|
4670
|
-
import { AbsoluteFill, useCurrentFrame, spring, useVideoConfig } from "remotion";
|
|
4671
|
-
|
|
4672
|
-
export const DASHBOARD_SCENE_DURATION = 150;
|
|
4505
|
+
import { createTikTokStyleCaptions } from "@remotion/captions";
|
|
4673
4506
|
|
|
4674
|
-
const
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4507
|
+
const { pages } = createTikTokStyleCaptions({
|
|
4508
|
+
captions: transcriptCaptions,
|
|
4509
|
+
combineTokensWithinMilliseconds: 1200, // Higher = more words per page
|
|
4510
|
+
});
|
|
4511
|
+
\`\`\`
|
|
4679
4512
|
|
|
4680
|
-
|
|
4681
|
-
const frame = useCurrentFrame();
|
|
4682
|
-
const { fps } = useVideoConfig();
|
|
4513
|
+
### Word-by-Word Highlighting
|
|
4683
4514
|
|
|
4515
|
+
\`\`\`tsx
|
|
4516
|
+
{page.tokens.map((token) => {
|
|
4517
|
+
const isActive = currentTimeMs >= token.fromMs && currentTimeMs < token.toMs;
|
|
4684
4518
|
return (
|
|
4685
|
-
<
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4519
|
+
<span style={{
|
|
4520
|
+
color: isActive ? "#FFD700" : "#fff",
|
|
4521
|
+
fontSize: 64,
|
|
4522
|
+
fontWeight: 900,
|
|
4523
|
+
textTransform: "uppercase",
|
|
4524
|
+
}}>
|
|
4525
|
+
{token.text}
|
|
4526
|
+
</span>
|
|
4689
4527
|
);
|
|
4690
|
-
}
|
|
4528
|
+
})}
|
|
4691
4529
|
\`\`\`
|
|
4692
4530
|
|
|
4693
|
-
|
|
4531
|
+
---
|
|
4694
4532
|
|
|
4695
|
-
|
|
4696
|
-
${cmd2} video create \\
|
|
4697
|
-
--script "Introducing [App]. The fastest way to..." \\
|
|
4698
|
-
--music-prompt "modern uplifting tech" \\
|
|
4699
|
-
--output ./public
|
|
4700
|
-
\`\`\`
|
|
4533
|
+
## 7. Script Structure (60 Seconds)
|
|
4701
4534
|
|
|
4702
|
-
|
|
4535
|
+
| Timestamp | Section | Purpose |
|
|
4536
|
+
|-----------|---------|---------|
|
|
4537
|
+
| 0:00-0:03 | **HOOK** | Pattern interrupt, bold claim |
|
|
4538
|
+
| 0:03-0:10 | **PROBLEM** | Agitate the pain point |
|
|
4539
|
+
| 0:10-0:40 | **SOLUTION** | Rapid value delivery |
|
|
4540
|
+
| 0:40-0:55 | **PROOF** | Show, don't tell |
|
|
4541
|
+
| 0:55-0:60 | **CTA** | Soft ask (Follow/Link in bio) |
|
|
4703
4542
|
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4543
|
+
### CTA Best Practices
|
|
4544
|
+
|
|
4545
|
+
| Do | Don't |
|
|
4546
|
+
|----|-------|
|
|
4547
|
+
| "Follow for more tips" | "SMASH that like button" |
|
|
4548
|
+
| "Link in bio" | "Click the link below" |
|
|
4549
|
+
| "Save this for later" | "Subscribe to my channel" |
|
|
4708
4550
|
|
|
4709
4551
|
---
|
|
4710
4552
|
|
|
4711
|
-
##
|
|
4553
|
+
## 8. Quick Reference
|
|
4712
4554
|
|
|
4713
|
-
|
|
4714
|
-
2. **Use mock data** - Pre-define realistic demo data
|
|
4715
|
-
3. **Match voiceover timing** - Sync visual transitions with narration
|
|
4716
|
-
4. **Keep scenes focused** - One main idea per scene
|
|
4717
|
-
5. **Test at 1x speed** - Preview at normal speed to catch timing issues
|
|
4718
|
-
`;
|
|
4719
|
-
}
|
|
4720
|
-
function generateAllSkillFiles(b) {
|
|
4721
|
-
return {
|
|
4722
|
-
"SKILL.md": generateSkillContent(b),
|
|
4723
|
-
"rules/presentations.md": generatePresentationsRule(b),
|
|
4724
|
-
"rules/video.md": generateVideoRule(b),
|
|
4725
|
-
"rules/motion-standards.md": generateMotionStandardsRule(),
|
|
4726
|
-
"rules/micro-interactions.md": generateMicroInteractionsRule(),
|
|
4727
|
-
"rules/component-integration.md": generateComponentIntegrationRule(b),
|
|
4728
|
-
"rules/project-video-workflow.md": generateProjectVideoWorkflowRule(b),
|
|
4729
|
-
"assets/animation-components.tsx": generateAnimationComponents()
|
|
4730
|
-
};
|
|
4731
|
-
}
|
|
4732
|
-
var EDITORS = [
|
|
4733
|
-
{ name: "Claude Code", dir: ".claude" },
|
|
4734
|
-
{ name: "Cursor", dir: ".cursor" },
|
|
4735
|
-
{ name: "Codex", dir: ".codex" },
|
|
4736
|
-
{ name: "OpenCode", dir: ".opencode" },
|
|
4737
|
-
{ name: "Windsurf", dir: ".windsurf" },
|
|
4738
|
-
{ name: "Agent", dir: ".agent" }
|
|
4739
|
-
];
|
|
4740
|
-
var skillCommand = new Command14("skill").description(`Manage ${brand.displayName} skill for AI coding assistants`).addHelpText(
|
|
4741
|
-
"after",
|
|
4742
|
-
`
|
|
4743
|
-
${chalk12.bold("Examples:")}
|
|
4744
|
-
${chalk12.gray("# Install skill for all detected editors")}
|
|
4745
|
-
$ ${brand.name} skill install
|
|
4555
|
+
### Platform Specs
|
|
4746
4556
|
|
|
4747
|
-
|
|
4748
|
-
|
|
4557
|
+
| Platform | Ratio | Resolution | Duration | FPS |
|
|
4558
|
+
|----------|-------|------------|----------|-----|
|
|
4559
|
+
| TikTok | 9:16 | 1080x1920 | 15-60s | 30 |
|
|
4560
|
+
| Reels | 9:16 | 1080x1920 | 15-90s | 30 |
|
|
4561
|
+
| Shorts | 9:16 | 1080x1920 | 15-60s | 30 |
|
|
4562
|
+
| YouTube | 16:9 | 1920x1080 | any | 30/60 |
|
|
4749
4563
|
|
|
4750
|
-
|
|
4751
|
-
$ ${brand.name} skill install --skip-remotion
|
|
4564
|
+
### Timing (30fps)
|
|
4752
4565
|
|
|
4753
|
-
|
|
4754
|
-
|
|
4566
|
+
| Action | Frames | Seconds |
|
|
4567
|
+
|--------|--------|---------|
|
|
4568
|
+
| Hook impact | 0-3 | 0-0.1s |
|
|
4569
|
+
| Text hold min | 45 | 1.5s |
|
|
4570
|
+
| Scene change max | 90 | 3s |
|
|
4571
|
+
| Ideal scene | 60 | 2s |
|
|
4572
|
+
|
|
4573
|
+
### Self-Check
|
|
4574
|
+
|
|
4575
|
+
- [ ] Text in FRAME 1 (no millennial pause)
|
|
4576
|
+
- [ ] Visual change every 2-3 seconds
|
|
4577
|
+
- [ ] All content within safe zones
|
|
4578
|
+
- [ ] Captions at bottom: 350px+
|
|
4579
|
+
- [ ] No content in bottom 20% or right 120px
|
|
4580
|
+
- [ ] Lo-fi aesthetic (not TV ad)
|
|
4581
|
+
- [ ] CTA is soft, not desperate
|
|
4582
|
+
- [ ] Duration 15-60 seconds
|
|
4755
4583
|
`
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4584
|
+
}
|
|
4585
|
+
];
|
|
4586
|
+
|
|
4587
|
+
// src/commands/skill/installer.ts
|
|
4588
|
+
function validatePath(basePath, targetPath) {
|
|
4589
|
+
const resolvedBase = resolve4(basePath);
|
|
4590
|
+
const resolvedTarget = resolve4(basePath, targetPath);
|
|
4591
|
+
const relativePath = relative(resolvedBase, resolvedTarget);
|
|
4592
|
+
if (relativePath.startsWith("..") || resolve4(resolvedTarget) !== resolvedTarget.replace(/\.\./g, "")) {
|
|
4593
|
+
throw new Error(`Invalid path: "${targetPath}" would escape base directory`);
|
|
4594
|
+
}
|
|
4595
|
+
return resolvedTarget;
|
|
4596
|
+
}
|
|
4597
|
+
function getRuleFiles() {
|
|
4598
|
+
const rules = [];
|
|
4599
|
+
for (const rule of VIDEO_RULE_CONTENTS) {
|
|
4600
|
+
rules.push({
|
|
4601
|
+
path: `rules/video/${rule.filename}`,
|
|
4602
|
+
content: rule.content
|
|
4603
|
+
});
|
|
4604
|
+
}
|
|
4605
|
+
return rules;
|
|
4606
|
+
}
|
|
4607
|
+
function installSkillToPath(skillPath, content) {
|
|
4608
|
+
const skillFile = join(skillPath, "SKILL.md");
|
|
4609
|
+
mkdirSync(skillPath, { recursive: true });
|
|
4610
|
+
writeFileSync(skillFile, content, "utf-8");
|
|
4611
|
+
const rules = getRuleFiles();
|
|
4612
|
+
for (const rule of rules) {
|
|
4613
|
+
const rulePath = join(skillPath, rule.path);
|
|
4614
|
+
const ruleDir = join(skillPath, rule.path.split("/").slice(0, -1).join("/"));
|
|
4615
|
+
mkdirSync(ruleDir, { recursive: true });
|
|
4616
|
+
writeFileSync(rulePath, rule.content, "utf-8");
|
|
4617
|
+
}
|
|
4618
|
+
}
|
|
4619
|
+
function installSkill(skillName, content, options = {}) {
|
|
4620
|
+
const result = {
|
|
4621
|
+
installed: [],
|
|
4622
|
+
skipped: [],
|
|
4623
|
+
errors: []
|
|
4624
|
+
};
|
|
4761
4625
|
const baseDir = options.local ? process.cwd() : homedir();
|
|
4762
|
-
const skillFiles = generateAllSkillFiles(brand);
|
|
4763
4626
|
if (options.dir) {
|
|
4764
|
-
const skillPath = join(options.dir, "skills", brand.name);
|
|
4765
4627
|
try {
|
|
4766
|
-
|
|
4767
|
-
|
|
4628
|
+
const resolvedDir = resolve4(options.dir);
|
|
4629
|
+
const skillPath = validatePath(resolvedDir, join("skills", skillName));
|
|
4630
|
+
installSkillToPath(skillPath, content);
|
|
4631
|
+
result.installed.push(options.dir);
|
|
4768
4632
|
} catch (err) {
|
|
4769
|
-
errors.push(`${options.dir}: ${err instanceof Error ? err.message : String(err)}`);
|
|
4633
|
+
result.errors.push(`${options.dir}: ${err instanceof Error ? err.message : String(err)}`);
|
|
4770
4634
|
}
|
|
4771
4635
|
} else {
|
|
4772
|
-
for (const editor of
|
|
4636
|
+
for (const editor of SUPPORTED_EDITORS) {
|
|
4773
4637
|
const editorDir = join(baseDir, editor.dir);
|
|
4774
|
-
const skillPath = join(editorDir, "skills",
|
|
4638
|
+
const skillPath = join(editorDir, "skills", skillName);
|
|
4775
4639
|
const skillFile = join(skillPath, "SKILL.md");
|
|
4776
4640
|
if (!existsSync2(editorDir)) {
|
|
4777
4641
|
continue;
|
|
4778
4642
|
}
|
|
4779
4643
|
if (existsSync2(skillFile) && !options.force) {
|
|
4780
|
-
skipped.push(editor.name);
|
|
4644
|
+
result.skipped.push(editor.name);
|
|
4781
4645
|
continue;
|
|
4782
4646
|
}
|
|
4783
4647
|
try {
|
|
4784
|
-
|
|
4785
|
-
installed.push(editor.name);
|
|
4648
|
+
installSkillToPath(skillPath, content);
|
|
4649
|
+
result.installed.push(editor.name);
|
|
4650
|
+
} catch (err) {
|
|
4651
|
+
result.errors.push(`${editor.name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
4652
|
+
}
|
|
4653
|
+
}
|
|
4654
|
+
}
|
|
4655
|
+
return result;
|
|
4656
|
+
}
|
|
4657
|
+
function uninstallSkill(skillName, options = {}) {
|
|
4658
|
+
const result = {
|
|
4659
|
+
removed: [],
|
|
4660
|
+
errors: []
|
|
4661
|
+
};
|
|
4662
|
+
const baseDir = options.local ? process.cwd() : homedir();
|
|
4663
|
+
for (const editor of SUPPORTED_EDITORS) {
|
|
4664
|
+
const skillPath = join(baseDir, editor.dir, "skills", skillName);
|
|
4665
|
+
if (existsSync2(skillPath)) {
|
|
4666
|
+
try {
|
|
4667
|
+
rmSync(skillPath, { recursive: true });
|
|
4668
|
+
result.removed.push(editor.name);
|
|
4786
4669
|
} catch (err) {
|
|
4787
|
-
errors.push(`${editor.name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
4670
|
+
result.errors.push(`${editor.name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
4788
4671
|
}
|
|
4789
4672
|
}
|
|
4790
4673
|
}
|
|
4674
|
+
return result;
|
|
4675
|
+
}
|
|
4676
|
+
function getSupportedEditorNames() {
|
|
4677
|
+
return SUPPORTED_EDITORS.map((e) => e.name);
|
|
4678
|
+
}
|
|
4679
|
+
|
|
4680
|
+
// src/commands/skill/index.ts
|
|
4681
|
+
var skillCommand = new Command14("skill").description(`Manage ${brand.displayName} skill for AI coding assistants`).addHelpText(
|
|
4682
|
+
"after",
|
|
4683
|
+
`
|
|
4684
|
+
${chalk12.bold("Examples:")}
|
|
4685
|
+
${chalk12.gray("# Install skill for all detected editors")}
|
|
4686
|
+
$ ${brand.name} skill install
|
|
4687
|
+
|
|
4688
|
+
${chalk12.gray("# Install to specific directory")}
|
|
4689
|
+
$ ${brand.name} skill install --dir ~/.claude
|
|
4690
|
+
|
|
4691
|
+
${chalk12.gray("# Show skill content")}
|
|
4692
|
+
$ ${brand.name} skill show
|
|
4693
|
+
`
|
|
4694
|
+
);
|
|
4695
|
+
skillCommand.command("install").description(`Install the ${brand.displayName} skill for AI coding assistants`).option("-d, --dir <path>", "Install to specific directory").option("-g, --global", "Install globally (to home directory)", true).option("-l, --local", "Install locally (to current directory)").option("-f, --force", "Overwrite existing skill files").action(async (options) => {
|
|
4696
|
+
const skillContent = generateSkillContent(brand);
|
|
4697
|
+
const result = installSkill(brand.name, skillContent, {
|
|
4698
|
+
dir: options.dir,
|
|
4699
|
+
local: options.local,
|
|
4700
|
+
force: options.force
|
|
4701
|
+
});
|
|
4791
4702
|
console.log();
|
|
4792
|
-
if (installed.length > 0) {
|
|
4703
|
+
if (result.installed.length > 0) {
|
|
4793
4704
|
success("Skill installed successfully");
|
|
4794
4705
|
console.log();
|
|
4795
|
-
keyValue("Installed to", installed.join(", "));
|
|
4796
|
-
keyValue("Files", Object.keys(skillFiles).length.toString());
|
|
4706
|
+
keyValue("Installed to", result.installed.join(", "));
|
|
4797
4707
|
}
|
|
4798
|
-
if (skipped.length > 0) {
|
|
4708
|
+
if (result.skipped.length > 0) {
|
|
4799
4709
|
console.log();
|
|
4800
|
-
info(`Skipped (already exists): ${skipped.join(", ")}`);
|
|
4710
|
+
info(`Skipped (already exists): ${result.skipped.join(", ")}`);
|
|
4801
4711
|
console.log(chalk12.gray(" Use --force to overwrite"));
|
|
4802
4712
|
}
|
|
4803
|
-
if (errors.length > 0) {
|
|
4713
|
+
if (result.errors.length > 0) {
|
|
4804
4714
|
console.log();
|
|
4805
|
-
for (const err of errors) {
|
|
4715
|
+
for (const err of result.errors) {
|
|
4806
4716
|
error(err);
|
|
4807
4717
|
}
|
|
4808
4718
|
}
|
|
4809
|
-
if (installed.length === 0 && skipped.length === 0 && errors.length === 0) {
|
|
4719
|
+
if (result.installed.length === 0 && result.skipped.length === 0 && result.errors.length === 0) {
|
|
4810
4720
|
info("No supported AI coding assistants detected.");
|
|
4811
4721
|
console.log();
|
|
4812
|
-
console.log(chalk12.gray("Supported editors: " +
|
|
4722
|
+
console.log(chalk12.gray("Supported editors: " + getSupportedEditorNames().join(", ")));
|
|
4813
4723
|
console.log(chalk12.gray("Use --dir <path> to install to a specific directory"));
|
|
4814
4724
|
}
|
|
4815
|
-
if (installed.length > 0 && !options.skipRemotion) {
|
|
4816
|
-
console.log();
|
|
4817
|
-
info("Installing remotion-best-practices skill...");
|
|
4818
|
-
try {
|
|
4819
|
-
execSync("npx -y skills add https://github.com/remotion-dev/skills --skill remotion-best-practices --all", {
|
|
4820
|
-
stdio: "inherit",
|
|
4821
|
-
timeout: 6e4
|
|
4822
|
-
});
|
|
4823
|
-
success("remotion-best-practices skill installed");
|
|
4824
|
-
} catch (err) {
|
|
4825
|
-
warn("Could not install remotion-best-practices skill automatically");
|
|
4826
|
-
console.log(chalk12.gray(" Run manually: npx skills add remotion-dev/skills"));
|
|
4827
|
-
}
|
|
4828
|
-
}
|
|
4829
4725
|
console.log();
|
|
4830
4726
|
});
|
|
4831
|
-
skillCommand.command("show").description("Display the skill content").
|
|
4832
|
-
|
|
4833
|
-
if (options.all) {
|
|
4834
|
-
for (const [path2, content] of Object.entries(files)) {
|
|
4835
|
-
console.log(chalk12.bold.cyan(`
|
|
4836
|
-
=== ${path2} ===
|
|
4837
|
-
`));
|
|
4838
|
-
console.log(content);
|
|
4839
|
-
}
|
|
4840
|
-
} else {
|
|
4841
|
-
console.log(files["SKILL.md"]);
|
|
4842
|
-
console.log(chalk12.gray("\nUse --all to show all files"));
|
|
4843
|
-
}
|
|
4727
|
+
skillCommand.command("show").description("Display the skill content").action(() => {
|
|
4728
|
+
console.log(generateSkillContent(brand));
|
|
4844
4729
|
});
|
|
4845
4730
|
skillCommand.command("uninstall").description(`Remove the ${brand.displayName} skill from AI coding assistants`).option("-g, --global", "Uninstall globally (from home directory)", true).option("-l, --local", "Uninstall locally (from current directory)").action(async (options) => {
|
|
4846
|
-
const
|
|
4847
|
-
const removed = [];
|
|
4848
|
-
const baseDir = options.local ? process.cwd() : homedir();
|
|
4849
|
-
for (const editor of EDITORS) {
|
|
4850
|
-
const skillPath = join(baseDir, editor.dir, "skills", brand.name);
|
|
4851
|
-
if (existsSync2(skillPath)) {
|
|
4852
|
-
try {
|
|
4853
|
-
rmSync(skillPath, { recursive: true });
|
|
4854
|
-
removed.push(editor.name);
|
|
4855
|
-
} catch {
|
|
4856
|
-
}
|
|
4857
|
-
}
|
|
4858
|
-
}
|
|
4731
|
+
const result = uninstallSkill(brand.name, { local: options.local });
|
|
4859
4732
|
console.log();
|
|
4860
|
-
if (removed.length > 0) {
|
|
4733
|
+
if (result.removed.length > 0) {
|
|
4861
4734
|
success("Skill uninstalled");
|
|
4862
|
-
keyValue("Removed from", removed.join(", "));
|
|
4735
|
+
keyValue("Removed from", result.removed.join(", "));
|
|
4863
4736
|
} else {
|
|
4864
4737
|
info("No installed skills found");
|
|
4865
4738
|
}
|
|
4739
|
+
if (result.errors.length > 0) {
|
|
4740
|
+
for (const err of result.errors) {
|
|
4741
|
+
warn(`Failed to remove: ${err}`);
|
|
4742
|
+
}
|
|
4743
|
+
}
|
|
4866
4744
|
console.log();
|
|
4867
4745
|
});
|
|
4868
|
-
function installSkill(skillPath, files, force) {
|
|
4869
|
-
mkdirSync(join(skillPath, "rules"), { recursive: true });
|
|
4870
|
-
mkdirSync(join(skillPath, "assets"), { recursive: true });
|
|
4871
|
-
for (const [relativePath, content] of Object.entries(files)) {
|
|
4872
|
-
const filePath = join(skillPath, relativePath);
|
|
4873
|
-
writeFileSync(filePath, content, "utf-8");
|
|
4874
|
-
}
|
|
4875
|
-
}
|
|
4876
4746
|
|
|
4877
4747
|
// src/commands/tts.ts
|
|
4878
4748
|
init_api();
|
|
@@ -5300,8 +5170,8 @@ init_types();
|
|
|
5300
5170
|
import { Command as Command19 } from "commander";
|
|
5301
5171
|
import ora12 from "ora";
|
|
5302
5172
|
import { mkdir, writeFile as writeFile5, readFile as readFile2, access, rm } from "fs/promises";
|
|
5303
|
-
import { join as join2, resolve as
|
|
5304
|
-
import { execSync
|
|
5173
|
+
import { join as join2, resolve as resolve5 } from "path";
|
|
5174
|
+
import { execSync, spawn } from "child_process";
|
|
5305
5175
|
var DEFAULT_TEMPLATE = "inizio-inc/remotion-composition";
|
|
5306
5176
|
var DEFAULT_FPS = 30;
|
|
5307
5177
|
function parseScriptIntoSections(script) {
|
|
@@ -5391,6 +5261,26 @@ function calculateSectionTimingFromTimestamps(sections, timestamps, fps) {
|
|
|
5391
5261
|
}
|
|
5392
5262
|
return results;
|
|
5393
5263
|
}
|
|
5264
|
+
async function readStdin2() {
|
|
5265
|
+
if (process.stdin.isTTY) {
|
|
5266
|
+
return null;
|
|
5267
|
+
}
|
|
5268
|
+
return new Promise((resolve6) => {
|
|
5269
|
+
let data = "";
|
|
5270
|
+
process.stdin.setEncoding("utf-8");
|
|
5271
|
+
process.stdin.on("data", (chunk) => {
|
|
5272
|
+
data += chunk;
|
|
5273
|
+
});
|
|
5274
|
+
process.stdin.on("end", () => resolve6(data.trim() || null));
|
|
5275
|
+
process.stdin.on("error", () => resolve6(null));
|
|
5276
|
+
setTimeout(() => {
|
|
5277
|
+
if (!data) resolve6(null);
|
|
5278
|
+
}, 100);
|
|
5279
|
+
});
|
|
5280
|
+
}
|
|
5281
|
+
function toFilename(name) {
|
|
5282
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
5283
|
+
}
|
|
5394
5284
|
async function downloadFile3(url, outputPath) {
|
|
5395
5285
|
if (url.startsWith("data:")) {
|
|
5396
5286
|
const matches = url.match(/^data:[^;]+;base64,(.+)$/);
|
|
@@ -5420,72 +5310,199 @@ function getExtension(url) {
|
|
|
5420
5310
|
}
|
|
5421
5311
|
return "jpg";
|
|
5422
5312
|
}
|
|
5423
|
-
var
|
|
5313
|
+
var createCommand3 = new Command19("create").description("Create video assets (voiceover per scene, music, images)").option("-s, --script <text>", "Narration script (legacy single-script mode)").option("--script-file <path>", "Path to script file (legacy) or scenes JSON").option("-t, --topic <text>", "Topic for image search").option("-v, --voice <name>", "TTS voice (Kore, Puck, Rachel, alloy)", "Kore").option("-m, --music-prompt <text>", "Music description").option("-n, --num-images <number>", "Number of images to search/download", "5").option("-o, --output <dir>", "Output directory", "./public").option("-f, --format <format>", "Output format: human, json, quiet", "human").action(async (options) => {
|
|
5424
5314
|
const format = options.format;
|
|
5425
5315
|
const spinner = format === "human" ? ora12("Initializing...").start() : null;
|
|
5426
5316
|
try {
|
|
5427
|
-
|
|
5428
|
-
|
|
5317
|
+
const stdinData = await readStdin2();
|
|
5318
|
+
let scenesInput = null;
|
|
5319
|
+
if (stdinData) {
|
|
5429
5320
|
try {
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5321
|
+
const parsed = JSON.parse(stdinData);
|
|
5322
|
+
if (parsed.scenes && Array.isArray(parsed.scenes)) {
|
|
5323
|
+
scenesInput = parsed;
|
|
5324
|
+
}
|
|
5325
|
+
} catch {
|
|
5435
5326
|
}
|
|
5436
5327
|
}
|
|
5437
|
-
if (!
|
|
5438
|
-
|
|
5439
|
-
|
|
5440
|
-
|
|
5441
|
-
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
spinner?.stop();
|
|
5447
|
-
error("Number of images must be between 1 and 20");
|
|
5448
|
-
process.exit(EXIT_CODES.INVALID_INPUT);
|
|
5328
|
+
if (!scenesInput && options.scriptFile) {
|
|
5329
|
+
try {
|
|
5330
|
+
const fileContent = await readFile2(options.scriptFile, "utf-8");
|
|
5331
|
+
const parsed = JSON.parse(fileContent);
|
|
5332
|
+
if (parsed.scenes && Array.isArray(parsed.scenes)) {
|
|
5333
|
+
scenesInput = parsed;
|
|
5334
|
+
}
|
|
5335
|
+
} catch {
|
|
5336
|
+
}
|
|
5449
5337
|
}
|
|
5338
|
+
const voice = scenesInput?.voice || options.voice;
|
|
5339
|
+
const musicPrompt = scenesInput?.musicPrompt || options.musicPrompt || "uplifting background music, positive energy";
|
|
5450
5340
|
const audioDir = join2(options.output, "audio");
|
|
5451
5341
|
const imagesDir = join2(options.output, "images");
|
|
5342
|
+
const videosDir = join2(options.output, "videos");
|
|
5452
5343
|
if (spinner) spinner.text = "Creating directories...";
|
|
5453
5344
|
await mkdir(audioDir, { recursive: true });
|
|
5454
5345
|
await mkdir(imagesDir, { recursive: true });
|
|
5346
|
+
await mkdir(videosDir, { recursive: true });
|
|
5455
5347
|
let totalCost = 0;
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
|
|
5472
|
-
|
|
5473
|
-
|
|
5474
|
-
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
|
|
5348
|
+
let scenes = [];
|
|
5349
|
+
let totalDuration = 0;
|
|
5350
|
+
const allImages = [];
|
|
5351
|
+
const allVideos = [];
|
|
5352
|
+
if (scenesInput && scenesInput.scenes.length > 0) {
|
|
5353
|
+
if (format === "human") {
|
|
5354
|
+
spinner?.stop();
|
|
5355
|
+
info(`Processing ${scenesInput.scenes.length} scenes...`);
|
|
5356
|
+
spinner?.start();
|
|
5357
|
+
}
|
|
5358
|
+
let currentTime = 0;
|
|
5359
|
+
for (let i = 0; i < scenesInput.scenes.length; i++) {
|
|
5360
|
+
const scene = scenesInput.scenes[i];
|
|
5361
|
+
const filename = toFilename(scene.name);
|
|
5362
|
+
if (spinner) spinner.text = `[${scene.name}] Generating speech...`;
|
|
5363
|
+
const ttsResult = await generateSpeech({
|
|
5364
|
+
text: scene.script,
|
|
5365
|
+
options: { voice }
|
|
5366
|
+
});
|
|
5367
|
+
const audioPath = join2(audioDir, `${filename}.${ttsResult.format}`);
|
|
5368
|
+
await writeFile5(audioPath, ttsResult.audioData);
|
|
5369
|
+
totalCost += ttsResult.cost;
|
|
5370
|
+
const durationInSeconds = ttsResult.duration;
|
|
5371
|
+
const durationInFrames = Math.round(durationInSeconds * DEFAULT_FPS);
|
|
5372
|
+
const sceneData = {
|
|
5373
|
+
id: i + 1,
|
|
5374
|
+
name: scene.name,
|
|
5375
|
+
text: scene.script,
|
|
5376
|
+
wordCount: scene.script.split(/\s+/).length,
|
|
5377
|
+
startTime: currentTime,
|
|
5378
|
+
endTime: currentTime + durationInSeconds,
|
|
5379
|
+
durationInSeconds,
|
|
5380
|
+
durationInFrames,
|
|
5381
|
+
audioPath: `audio/${filename}.${ttsResult.format}`
|
|
5382
|
+
};
|
|
5383
|
+
if (scene.imageQuery) {
|
|
5384
|
+
if (spinner) spinner.text = `[${scene.name}] Searching image...`;
|
|
5385
|
+
try {
|
|
5386
|
+
const imageResults = await searchImages({
|
|
5387
|
+
query: scene.imageQuery,
|
|
5388
|
+
options: { maxResults: 1, size: "large", safeSearch: true }
|
|
5389
|
+
});
|
|
5390
|
+
const imgs = imageResults.data.results.flatMap((r) => r.results);
|
|
5391
|
+
totalCost += imageResults.data.totalCost;
|
|
5392
|
+
if (imgs.length > 0) {
|
|
5393
|
+
const img = imgs[0];
|
|
5394
|
+
const ext = getExtension(img.url);
|
|
5395
|
+
const imgFilename = `${filename}.${ext}`;
|
|
5396
|
+
const imgPath = join2(imagesDir, imgFilename);
|
|
5397
|
+
await downloadFile3(img.url, imgPath);
|
|
5398
|
+
sceneData.imagePath = `images/${imgFilename}`;
|
|
5399
|
+
allImages.push({
|
|
5400
|
+
path: `images/${imgFilename}`,
|
|
5401
|
+
url: img.url,
|
|
5402
|
+
width: img.width,
|
|
5403
|
+
height: img.height,
|
|
5404
|
+
query: scene.imageQuery
|
|
5405
|
+
});
|
|
5406
|
+
}
|
|
5407
|
+
} catch (err) {
|
|
5408
|
+
if (format === "human") {
|
|
5409
|
+
spinner?.stop();
|
|
5410
|
+
warn(`[${scene.name}] Image search failed: ${err instanceof Error ? err.message : "Unknown"}`);
|
|
5411
|
+
spinner?.start();
|
|
5412
|
+
}
|
|
5413
|
+
}
|
|
5414
|
+
}
|
|
5415
|
+
if (scene.videoQuery) {
|
|
5416
|
+
if (spinner) spinner.text = `[${scene.name}] Searching video...`;
|
|
5417
|
+
try {
|
|
5418
|
+
const videoResults = await searchVideos({
|
|
5419
|
+
query: scene.videoQuery,
|
|
5420
|
+
options: { maxResults: 1 }
|
|
5421
|
+
});
|
|
5422
|
+
const vids = videoResults.data.results.flatMap((r) => r.results);
|
|
5423
|
+
totalCost += videoResults.data.totalCost;
|
|
5424
|
+
if (vids.length > 0) {
|
|
5425
|
+
const vid = vids[0];
|
|
5426
|
+
const vidUrl = vid.previewUrl || vid.downloadUrl;
|
|
5427
|
+
if (vidUrl) {
|
|
5428
|
+
const vidFilename = `${filename}.mp4`;
|
|
5429
|
+
const vidPath = join2(videosDir, vidFilename);
|
|
5430
|
+
await downloadFile3(vidUrl, vidPath);
|
|
5431
|
+
sceneData.videoPath = `videos/${vidFilename}`;
|
|
5432
|
+
allVideos.push({
|
|
5433
|
+
path: `videos/${vidFilename}`,
|
|
5434
|
+
url: vidUrl,
|
|
5435
|
+
width: vid.width,
|
|
5436
|
+
height: vid.height,
|
|
5437
|
+
duration: vid.duration,
|
|
5438
|
+
query: scene.videoQuery
|
|
5439
|
+
});
|
|
5440
|
+
}
|
|
5441
|
+
}
|
|
5442
|
+
} catch (err) {
|
|
5443
|
+
if (format === "human") {
|
|
5444
|
+
spinner?.stop();
|
|
5445
|
+
warn(`[${scene.name}] Video search failed: ${err instanceof Error ? err.message : "Unknown"}`);
|
|
5446
|
+
spinner?.start();
|
|
5447
|
+
}
|
|
5448
|
+
}
|
|
5449
|
+
}
|
|
5450
|
+
scenes.push(sceneData);
|
|
5451
|
+
currentTime += durationInSeconds;
|
|
5452
|
+
totalDuration += durationInSeconds;
|
|
5453
|
+
if (format === "human") {
|
|
5454
|
+
spinner?.stop();
|
|
5455
|
+
const assets = [
|
|
5456
|
+
`audio: ${durationInSeconds.toFixed(1)}s`,
|
|
5457
|
+
sceneData.imagePath ? "image" : null,
|
|
5458
|
+
sceneData.videoPath ? "video" : null
|
|
5459
|
+
].filter(Boolean).join(", ");
|
|
5460
|
+
success(` ${scene.name}: ${assets}`);
|
|
5461
|
+
spinner?.start();
|
|
5462
|
+
}
|
|
5463
|
+
}
|
|
5464
|
+
} else {
|
|
5465
|
+
let script = options.script;
|
|
5466
|
+
if (options.scriptFile) {
|
|
5467
|
+
try {
|
|
5468
|
+
script = await readFile2(options.scriptFile, "utf-8");
|
|
5469
|
+
} catch (err) {
|
|
5470
|
+
spinner?.stop();
|
|
5471
|
+
error(`Failed to read script file: ${err instanceof Error ? err.message : "Unknown error"}`);
|
|
5472
|
+
process.exit(EXIT_CODES.INVALID_INPUT);
|
|
5473
|
+
}
|
|
5474
|
+
}
|
|
5475
|
+
if (!script || script.trim().length === 0) {
|
|
5476
|
+
spinner?.stop();
|
|
5477
|
+
error("Provide scenes via stdin JSON, --script-file with scenes JSON, or --script for legacy mode");
|
|
5478
|
+
process.exit(EXIT_CODES.INVALID_INPUT);
|
|
5479
|
+
}
|
|
5480
|
+
script = script.trim();
|
|
5481
|
+
const _topic = options.topic || script.split(".")[0].slice(0, 50);
|
|
5482
|
+
void _topic;
|
|
5483
|
+
if (spinner) spinner.text = "Generating voiceover...";
|
|
5484
|
+
const ttsResult = await generateSpeech({
|
|
5485
|
+
text: script,
|
|
5486
|
+
options: { voice }
|
|
5487
|
+
});
|
|
5488
|
+
const voiceoverPath = join2(audioDir, `voiceover.${ttsResult.format}`);
|
|
5489
|
+
await writeFile5(voiceoverPath, ttsResult.audioData);
|
|
5490
|
+
totalCost += ttsResult.cost;
|
|
5491
|
+
totalDuration = ttsResult.duration;
|
|
5492
|
+
const sectionTexts = parseScriptIntoSections(script);
|
|
5493
|
+
const sectionsWithTiming = calculateSectionTiming(sectionTexts, ttsResult.duration, DEFAULT_FPS, ttsResult.timestamps);
|
|
5494
|
+
scenes = sectionsWithTiming.map((s, i) => ({
|
|
5495
|
+
...s,
|
|
5496
|
+
name: `Section${i + 1}`,
|
|
5497
|
+
audioPath: `audio/voiceover.${ttsResult.format}`
|
|
5498
|
+
}));
|
|
5499
|
+
if (format === "human") {
|
|
5500
|
+
spinner?.stop();
|
|
5501
|
+
success(`Voiceover: ${voiceoverPath} (${ttsResult.duration.toFixed(1)}s)`);
|
|
5502
|
+
spinner?.start();
|
|
5503
|
+
}
|
|
5486
5504
|
}
|
|
5487
|
-
const musicDuration = Math.min(30, Math.ceil(
|
|
5488
|
-
const musicPrompt = options.musicPrompt || "uplifting background music, positive energy";
|
|
5505
|
+
const musicDuration = Math.min(30, Math.ceil(totalDuration) + 5);
|
|
5489
5506
|
if (spinner) spinner.text = "Generating music...";
|
|
5490
5507
|
let musicResult = await generateMusic({
|
|
5491
5508
|
prompt: musicPrompt,
|
|
@@ -5520,67 +5537,13 @@ var createCommand2 = new Command19("create").description("Create video assets (v
|
|
|
5520
5537
|
success(`Music: ${musicPath} (${musicInfo.duration}s)`);
|
|
5521
5538
|
spinner?.start();
|
|
5522
5539
|
}
|
|
5523
|
-
if (spinner) spinner.text = "Searching for images...";
|
|
5524
|
-
const imageResults = await searchImages({
|
|
5525
|
-
query: topic,
|
|
5526
|
-
options: {
|
|
5527
|
-
maxResults: numImages,
|
|
5528
|
-
size: "large",
|
|
5529
|
-
safeSearch: true
|
|
5530
|
-
}
|
|
5531
|
-
});
|
|
5532
|
-
const allImages = imageResults.data.results.flatMap(
|
|
5533
|
-
(providerResult) => providerResult.results.map((img) => ({
|
|
5534
|
-
...img,
|
|
5535
|
-
provider: providerResult.providerName
|
|
5536
|
-
}))
|
|
5537
|
-
);
|
|
5538
|
-
totalCost += imageResults.data.totalCost;
|
|
5539
|
-
const downloadedImages = [];
|
|
5540
|
-
for (let i = 0; i < Math.min(allImages.length, numImages); i++) {
|
|
5541
|
-
const img = allImages[i];
|
|
5542
|
-
const ext = getExtension(img.url);
|
|
5543
|
-
const filename = `scene-${i + 1}.${ext}`;
|
|
5544
|
-
const imagePath = join2(imagesDir, filename);
|
|
5545
|
-
if (spinner) spinner.text = `Downloading image ${i + 1}/${Math.min(allImages.length, numImages)}...`;
|
|
5546
|
-
try {
|
|
5547
|
-
await downloadFile3(img.url, imagePath);
|
|
5548
|
-
downloadedImages.push({
|
|
5549
|
-
path: `images/${filename}`,
|
|
5550
|
-
url: img.url,
|
|
5551
|
-
width: img.width,
|
|
5552
|
-
height: img.height,
|
|
5553
|
-
query: topic
|
|
5554
|
-
});
|
|
5555
|
-
} catch (err) {
|
|
5556
|
-
if (format === "human") {
|
|
5557
|
-
spinner?.stop();
|
|
5558
|
-
warn(`Failed to download image ${i + 1}: ${err instanceof Error ? err.message : "Unknown error"}`);
|
|
5559
|
-
spinner?.start();
|
|
5560
|
-
}
|
|
5561
|
-
}
|
|
5562
|
-
}
|
|
5563
|
-
if (format === "human") {
|
|
5564
|
-
spinner?.stop();
|
|
5565
|
-
success(`Images: Downloaded ${downloadedImages.length} images to ${imagesDir}`);
|
|
5566
|
-
spinner?.start();
|
|
5567
|
-
}
|
|
5568
|
-
const sectionsWithImages = sections.map((section, index) => {
|
|
5569
|
-
const imageIndex = index % downloadedImages.length;
|
|
5570
|
-
return {
|
|
5571
|
-
...section,
|
|
5572
|
-
imagePath: downloadedImages[imageIndex]?.path
|
|
5573
|
-
};
|
|
5574
|
-
});
|
|
5575
5540
|
if (spinner) spinner.text = "Writing manifest...";
|
|
5576
|
-
const totalDurationInFrames = Math.round(
|
|
5541
|
+
const totalDurationInFrames = Math.round(totalDuration * DEFAULT_FPS);
|
|
5577
5542
|
const manifest = {
|
|
5578
|
-
topic,
|
|
5579
|
-
script,
|
|
5580
|
-
voiceover: voiceoverInfo,
|
|
5581
5543
|
music: musicInfo,
|
|
5582
|
-
images:
|
|
5583
|
-
|
|
5544
|
+
images: allImages,
|
|
5545
|
+
videos: allVideos,
|
|
5546
|
+
scenes,
|
|
5584
5547
|
totalDurationInFrames,
|
|
5585
5548
|
fps: DEFAULT_FPS,
|
|
5586
5549
|
totalCost,
|
|
@@ -5600,19 +5563,19 @@ var createCommand2 = new Command19("create").description("Create video assets (v
|
|
|
5600
5563
|
console.log();
|
|
5601
5564
|
success("Video assets created successfully!");
|
|
5602
5565
|
console.log();
|
|
5603
|
-
info(`
|
|
5604
|
-
|
|
5566
|
+
info(`Scenes: ${scenes.length} (${totalDurationInFrames} frames at ${DEFAULT_FPS}fps)`);
|
|
5567
|
+
for (const scene of scenes) {
|
|
5568
|
+
const assets = [
|
|
5569
|
+
scene.audioPath ? "audio" : null,
|
|
5570
|
+
scene.imagePath ? "image" : null,
|
|
5571
|
+
scene.videoPath ? "video" : null
|
|
5572
|
+
].filter(Boolean).join(", ");
|
|
5573
|
+
info(` - ${scene.name}: ${scene.durationInSeconds.toFixed(1)}s [${assets}]`);
|
|
5574
|
+
}
|
|
5605
5575
|
info(`Music: ${musicInfo.path} (${musicInfo.duration}s)`);
|
|
5606
|
-
info(`Sections: ${sections.length} (${totalDurationInFrames} frames at ${DEFAULT_FPS}fps)`);
|
|
5607
|
-
info(`Images: ${downloadedImages.length} downloaded`);
|
|
5608
5576
|
info(`Manifest: ${manifestPath}`);
|
|
5609
5577
|
console.log();
|
|
5610
5578
|
info(`Total cost: $${totalCost.toFixed(4)}`);
|
|
5611
|
-
console.log();
|
|
5612
|
-
info("Next steps:");
|
|
5613
|
-
info(" 1. Create Remotion scenes matching section timings in manifest");
|
|
5614
|
-
info(" 2. Each section has exact durationInFrames - use these for sync");
|
|
5615
|
-
info(" 3. Run: npx remotion render FullVideo out/video.mp4");
|
|
5616
5579
|
} catch (err) {
|
|
5617
5580
|
spinner?.stop();
|
|
5618
5581
|
error(err instanceof Error ? err.message : "Unknown error");
|
|
@@ -5663,11 +5626,11 @@ var searchCommand2 = new Command19("search").description("Search for stock video
|
|
|
5663
5626
|
process.exit(EXIT_CODES.GENERAL_ERROR);
|
|
5664
5627
|
}
|
|
5665
5628
|
});
|
|
5666
|
-
var initCommand = new Command19("init").description("Create a new Remotion video project from template").argument("<name>", "Project directory name").option("-t, --template <repo>", "GitHub repo (user/repo)", DEFAULT_TEMPLATE).option("--no-install", "Skip pnpm install").option("-f, --format <format>", "Output format: human, json, quiet", "human").action(async (name, options) => {
|
|
5629
|
+
var initCommand = new Command19("init").description("Create a new Remotion video project from template").argument("<name>", "Project directory name").option("-t, --template <repo>", "GitHub repo (user/repo)", DEFAULT_TEMPLATE).option("--type <type>", "Video type: landscape (16:9) or tiktok (9:16)", "landscape").option("--no-install", "Skip pnpm install").option("-f, --format <format>", "Output format: human, json, quiet", "human").action(async (name, options) => {
|
|
5667
5630
|
const format = options.format;
|
|
5668
5631
|
const spinner = format === "human" ? ora12("Initializing video project...").start() : null;
|
|
5669
5632
|
try {
|
|
5670
|
-
const targetDir =
|
|
5633
|
+
const targetDir = resolve5(process.cwd(), name);
|
|
5671
5634
|
try {
|
|
5672
5635
|
await access(targetDir);
|
|
5673
5636
|
spinner?.stop();
|
|
@@ -5675,14 +5638,22 @@ var initCommand = new Command19("init").description("Create a new Remotion video
|
|
|
5675
5638
|
process.exit(EXIT_CODES.INVALID_INPUT);
|
|
5676
5639
|
} catch {
|
|
5677
5640
|
}
|
|
5641
|
+
const templatePattern = /^[a-zA-Z0-9_-]+\/[a-zA-Z0-9_.-]+(\/[a-zA-Z0-9_.-]+)*(#[a-zA-Z0-9_.-]+)?$/;
|
|
5642
|
+
if (!templatePattern.test(options.template)) {
|
|
5643
|
+
spinner?.stop();
|
|
5644
|
+
error(`Invalid template format: "${options.template}". Expected format: owner/repo or owner/repo#branch`);
|
|
5645
|
+
process.exit(EXIT_CODES.INVALID_INPUT);
|
|
5646
|
+
}
|
|
5678
5647
|
if (spinner) spinner.text = `Downloading template from ${options.template}...`;
|
|
5679
5648
|
try {
|
|
5680
|
-
|
|
5649
|
+
execSync(`npx --yes degit ${options.template} "${targetDir}"`, {
|
|
5681
5650
|
stdio: "pipe"
|
|
5682
5651
|
});
|
|
5683
5652
|
} catch {
|
|
5684
5653
|
if (spinner) spinner.text = "Cloning template...";
|
|
5685
|
-
|
|
5654
|
+
const repoMatch = options.template.match(/^([a-zA-Z0-9_-]+\/[a-zA-Z0-9_.-]+)/);
|
|
5655
|
+
const repo = repoMatch ? repoMatch[1] : options.template;
|
|
5656
|
+
execSync(`git clone --depth 1 https://github.com/${repo}.git "${targetDir}"`, {
|
|
5686
5657
|
stdio: "pipe"
|
|
5687
5658
|
});
|
|
5688
5659
|
await rm(join2(targetDir, ".git"), { recursive: true, force: true });
|
|
@@ -5715,12 +5686,37 @@ var initCommand = new Command19("init").description("Create a new Remotion video
|
|
|
5715
5686
|
spinner?.start();
|
|
5716
5687
|
}
|
|
5717
5688
|
}
|
|
5689
|
+
if (options.type === "tiktok") {
|
|
5690
|
+
if (spinner) spinner.text = "Configuring for TikTok (9:16)...";
|
|
5691
|
+
const constantsPath = join2(targetDir, "types/constants.ts");
|
|
5692
|
+
try {
|
|
5693
|
+
let constantsContent = await readFile2(constantsPath, "utf-8");
|
|
5694
|
+
constantsContent = constantsContent.replace(
|
|
5695
|
+
/export const VIDEO_WIDTH = 1920;/,
|
|
5696
|
+
"export const VIDEO_WIDTH = 1080; // TikTok 9:16"
|
|
5697
|
+
).replace(
|
|
5698
|
+
/export const VIDEO_HEIGHT = 1080;/,
|
|
5699
|
+
"export const VIDEO_HEIGHT = 1920; // TikTok 9:16"
|
|
5700
|
+
).replace(
|
|
5701
|
+
/export const VIDEO_FPS = 60;/,
|
|
5702
|
+
"export const VIDEO_FPS = 30; // TikTok standard"
|
|
5703
|
+
);
|
|
5704
|
+
await writeFile5(constantsPath, constantsContent, "utf-8");
|
|
5705
|
+
if (format === "human") {
|
|
5706
|
+
spinner?.stop();
|
|
5707
|
+
success("Configured for TikTok (1080x1920 @ 30fps)");
|
|
5708
|
+
spinner?.start();
|
|
5709
|
+
}
|
|
5710
|
+
} catch {
|
|
5711
|
+
}
|
|
5712
|
+
}
|
|
5718
5713
|
spinner?.stop();
|
|
5719
5714
|
if (format === "json") {
|
|
5720
5715
|
printJson({
|
|
5721
5716
|
name,
|
|
5722
5717
|
path: targetDir,
|
|
5723
5718
|
template: options.template,
|
|
5719
|
+
type: options.type,
|
|
5724
5720
|
installed: options.install
|
|
5725
5721
|
});
|
|
5726
5722
|
return;
|
|
@@ -5731,6 +5727,11 @@ var initCommand = new Command19("init").description("Create a new Remotion video
|
|
|
5731
5727
|
}
|
|
5732
5728
|
console.log();
|
|
5733
5729
|
success(`Video project "${name}" created successfully!`);
|
|
5730
|
+
if (options.type === "tiktok") {
|
|
5731
|
+
info("Format: TikTok/Reels/Shorts (1080x1920 @ 30fps)");
|
|
5732
|
+
} else {
|
|
5733
|
+
info("Format: Landscape (1920x1080 @ 60fps)");
|
|
5734
|
+
}
|
|
5734
5735
|
console.log();
|
|
5735
5736
|
info("Next steps:");
|
|
5736
5737
|
info(` cd ${name}`);
|
|
@@ -5739,17 +5740,21 @@ var initCommand = new Command19("init").description("Create a new Remotion video
|
|
|
5739
5740
|
}
|
|
5740
5741
|
info(" pnpm dev # Preview in Remotion Studio");
|
|
5741
5742
|
info(" cc video create ... # Generate assets to public/");
|
|
5742
|
-
|
|
5743
|
+
if (options.type === "tiktok") {
|
|
5744
|
+
info(" pnpm exec remotion render TikTokVideo # Render TikTok video");
|
|
5745
|
+
} else {
|
|
5746
|
+
info(" pnpm exec remotion render FullVideo # Render final video");
|
|
5747
|
+
}
|
|
5743
5748
|
} catch (err) {
|
|
5744
5749
|
spinner?.stop();
|
|
5745
5750
|
error(err instanceof Error ? err.message : "Unknown error");
|
|
5746
5751
|
process.exit(EXIT_CODES.GENERAL_ERROR);
|
|
5747
5752
|
}
|
|
5748
5753
|
});
|
|
5749
|
-
var videoCommand = new Command19("video").description("Video asset generation commands").addCommand(initCommand).addCommand(
|
|
5754
|
+
var videoCommand = new Command19("video").description("Video asset generation commands").addCommand(initCommand).addCommand(createCommand3).addCommand(searchCommand2);
|
|
5750
5755
|
|
|
5751
5756
|
// src/index.ts
|
|
5752
|
-
var VERSION = "0.1.
|
|
5757
|
+
var VERSION = "0.1.7";
|
|
5753
5758
|
var program = new Command20();
|
|
5754
5759
|
var cmdName = brand.commands[0];
|
|
5755
5760
|
program.name(cmdName).description(brand.description).version(VERSION, "-v, --version", "Show version number").option("--debug", "Enable debug logging").option("--no-color", "Disable colored output").configureOutput({
|