@conceptcraft/mindframes 0.1.5 → 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 +2012 -372
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -946,12 +946,21 @@ async function generateSpeech(ttsRequest) {
|
|
|
946
946
|
const cost = parseFloat(response.headers.get("X-Cost-USD") || "0");
|
|
947
947
|
const provider = response.headers.get("X-Provider") || "unknown";
|
|
948
948
|
const format = response.headers.get("X-Audio-Format") || "mp3";
|
|
949
|
+
let timestamps;
|
|
950
|
+
const timestampsHeader = response.headers.get("X-Timestamps");
|
|
951
|
+
if (timestampsHeader) {
|
|
952
|
+
try {
|
|
953
|
+
timestamps = JSON.parse(timestampsHeader);
|
|
954
|
+
} catch {
|
|
955
|
+
}
|
|
956
|
+
}
|
|
949
957
|
return {
|
|
950
958
|
audioData,
|
|
951
959
|
duration,
|
|
952
960
|
cost,
|
|
953
961
|
provider,
|
|
954
|
-
format
|
|
962
|
+
format,
|
|
963
|
+
timestamps
|
|
955
964
|
};
|
|
956
965
|
}
|
|
957
966
|
async function getVoices() {
|
|
@@ -1016,7 +1025,7 @@ async function pollForCompletion(checkFn, maxAttempts = 60, intervalMs = 2e3) {
|
|
|
1016
1025
|
if (result.status === "completed" || result.status === "failed") {
|
|
1017
1026
|
return result;
|
|
1018
1027
|
}
|
|
1019
|
-
await new Promise((
|
|
1028
|
+
await new Promise((resolve6) => setTimeout(resolve6, intervalMs));
|
|
1020
1029
|
}
|
|
1021
1030
|
throw new ApiError("Operation timed out", 408, 1);
|
|
1022
1031
|
}
|
|
@@ -1123,10 +1132,10 @@ function generateState() {
|
|
|
1123
1132
|
async function findAvailablePort(start, end) {
|
|
1124
1133
|
for (let port = start; port <= end; port++) {
|
|
1125
1134
|
try {
|
|
1126
|
-
await new Promise((
|
|
1135
|
+
await new Promise((resolve6, reject) => {
|
|
1127
1136
|
const server = http.createServer();
|
|
1128
1137
|
server.listen(port, () => {
|
|
1129
|
-
server.close(() =>
|
|
1138
|
+
server.close(() => resolve6());
|
|
1130
1139
|
});
|
|
1131
1140
|
server.on("error", reject);
|
|
1132
1141
|
});
|
|
@@ -1184,7 +1193,7 @@ async function exchangeCodeForTokens(tokenEndpoint, code, codeVerifier, redirect
|
|
|
1184
1193
|
return response.json();
|
|
1185
1194
|
}
|
|
1186
1195
|
function startCallbackServer(port, expectedState) {
|
|
1187
|
-
return new Promise((
|
|
1196
|
+
return new Promise((resolve6, reject) => {
|
|
1188
1197
|
let timeoutId;
|
|
1189
1198
|
let settled = false;
|
|
1190
1199
|
const cleanup = () => {
|
|
@@ -1270,7 +1279,7 @@ function startCallbackServer(port, expectedState) {
|
|
|
1270
1279
|
</html>
|
|
1271
1280
|
`);
|
|
1272
1281
|
cleanup();
|
|
1273
|
-
|
|
1282
|
+
resolve6({ code, state });
|
|
1274
1283
|
});
|
|
1275
1284
|
server.listen(port);
|
|
1276
1285
|
process.once("SIGINT", onCancel);
|
|
@@ -2413,21 +2422,21 @@ Uploading ${options.file.length} file(s)...`));
|
|
|
2413
2422
|
}
|
|
2414
2423
|
});
|
|
2415
2424
|
async function readStdin() {
|
|
2416
|
-
return new Promise((
|
|
2425
|
+
return new Promise((resolve6) => {
|
|
2417
2426
|
let data = "";
|
|
2418
2427
|
process.stdin.setEncoding("utf8");
|
|
2419
2428
|
if (process.stdin.isTTY) {
|
|
2420
|
-
|
|
2429
|
+
resolve6("");
|
|
2421
2430
|
return;
|
|
2422
2431
|
}
|
|
2423
2432
|
process.stdin.on("data", (chunk) => {
|
|
2424
2433
|
data += chunk;
|
|
2425
2434
|
});
|
|
2426
2435
|
process.stdin.on("end", () => {
|
|
2427
|
-
|
|
2436
|
+
resolve6(data.trim());
|
|
2428
2437
|
});
|
|
2429
2438
|
setTimeout(() => {
|
|
2430
|
-
|
|
2439
|
+
resolve6(data.trim());
|
|
2431
2440
|
}, 100);
|
|
2432
2441
|
});
|
|
2433
2442
|
}
|
|
@@ -3118,44 +3127,59 @@ var whoamiCommand = new Command13("whoami").description("Show current user and t
|
|
|
3118
3127
|
}
|
|
3119
3128
|
});
|
|
3120
3129
|
|
|
3121
|
-
// src/commands/skill.ts
|
|
3130
|
+
// src/commands/skill/index.ts
|
|
3122
3131
|
init_output();
|
|
3123
3132
|
import { Command as Command14 } from "commander";
|
|
3124
3133
|
import chalk12 from "chalk";
|
|
3125
|
-
import { mkdirSync, writeFileSync, existsSync as existsSync2 } from "fs";
|
|
3126
|
-
import { join } from "path";
|
|
3127
|
-
import { homedir } from "os";
|
|
3128
|
-
function generateSkillContent(b) {
|
|
3129
|
-
const cmd2 = b.name;
|
|
3130
|
-
const pkg = b.packageName;
|
|
3131
|
-
const url = b.apiUrl;
|
|
3132
|
-
const name = b.displayName;
|
|
3133
|
-
return `---
|
|
3134
|
-
name: ${cmd2}
|
|
3135
|
-
description: Create AI-powered presentations from code, documentation, files, or any content. Use when the user wants to generate slides, presentations, or decks about their project, codebase, research, or ideas.
|
|
3136
|
-
---
|
|
3137
3134
|
|
|
3138
|
-
|
|
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
|
+
};
|
|
3143
|
+
|
|
3144
|
+
// src/commands/skill/sections/header.ts
|
|
3145
|
+
var header3 = {
|
|
3146
|
+
title: "Header",
|
|
3147
|
+
render: (ctx) => `# ${ctx.name} CLI
|
|
3139
3148
|
|
|
3140
|
-
Create professional presentations directly from your terminal. The CLI generates AI-powered slides from any context you provide - text, files, URLs, or piped content
|
|
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
|
+
};
|
|
3141
3151
|
|
|
3142
|
-
|
|
3152
|
+
// src/commands/skill/sections/prerequisites.ts
|
|
3153
|
+
var prerequisites = {
|
|
3154
|
+
title: "Prerequisites",
|
|
3155
|
+
render: (ctx) => `## Prerequisites
|
|
3143
3156
|
|
|
3144
3157
|
\`\`\`bash
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
# Configure API key (get from ${url}/settings/api-keys)
|
|
3149
|
-
${cmd2} config init
|
|
3158
|
+
npm install -g ${ctx.pkg}
|
|
3159
|
+
${ctx.cmd} login # Authenticate (opens browser)
|
|
3160
|
+
${ctx.cmd} whoami # Verify auth
|
|
3150
3161
|
\`\`\`
|
|
3151
3162
|
|
|
3152
|
-
|
|
3163
|
+
### Authentication
|
|
3164
|
+
|
|
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
|
+
};
|
|
3168
|
+
|
|
3169
|
+
// src/commands/skill/sections/workflow.ts
|
|
3170
|
+
var workflow = {
|
|
3171
|
+
title: "Core Workflow",
|
|
3172
|
+
render: (ctx) => `## Core Workflow
|
|
3153
3173
|
|
|
3154
3174
|
1. **Gather context** - Read relevant files, code, or documentation
|
|
3155
|
-
2. **Create presentation** - Pass context to \`${
|
|
3156
|
-
3. **Share URL** - Return the presentation link to the user
|
|
3175
|
+
2. **Create presentation** - Pass context to \`${ctx.cmd} create\`
|
|
3176
|
+
3. **Share URL** - Return the presentation link to the user`
|
|
3177
|
+
};
|
|
3157
3178
|
|
|
3158
|
-
|
|
3179
|
+
// src/commands/skill/sections/create-command.ts
|
|
3180
|
+
var createCommand2 = {
|
|
3181
|
+
title: "Create Command",
|
|
3182
|
+
render: (ctx) => `## Commands
|
|
3159
3183
|
|
|
3160
3184
|
### Create Presentation
|
|
3161
3185
|
|
|
@@ -3163,27 +3187,31 @@ Context is **required**. Provide it via one of these methods:
|
|
|
3163
3187
|
|
|
3164
3188
|
\`\`\`bash
|
|
3165
3189
|
# Upload files (PDFs, PPTX, images, docs)
|
|
3166
|
-
${
|
|
3190
|
+
${ctx.cmd} create "Product Overview" --file ./deck.pptx --file ./logo.png
|
|
3167
3191
|
|
|
3168
3192
|
# Direct text context
|
|
3169
|
-
${
|
|
3193
|
+
${ctx.cmd} create "Topic Title" --context "Key points, data, facts..."
|
|
3170
3194
|
|
|
3171
3195
|
# From a text file
|
|
3172
|
-
${
|
|
3196
|
+
${ctx.cmd} create "Topic Title" --context-file ./notes.md
|
|
3173
3197
|
|
|
3174
3198
|
# Pipe content (auto-detected)
|
|
3175
|
-
cat README.md | ${
|
|
3199
|
+
cat README.md | ${ctx.cmd} create "Project Overview"
|
|
3176
3200
|
|
|
3177
3201
|
# From URLs (scraped automatically)
|
|
3178
|
-
${
|
|
3202
|
+
${ctx.cmd} create "Competitor Analysis" --sources https://example.com/report
|
|
3179
3203
|
|
|
3180
3204
|
# Combine multiple sources
|
|
3181
|
-
cat src/auth/*.ts | ${
|
|
3205
|
+
cat src/auth/*.ts | ${ctx.cmd} create "Auth System" \\
|
|
3182
3206
|
--file ./architecture.png \\
|
|
3183
3207
|
--context "Focus on security patterns"
|
|
3184
|
-
|
|
3208
|
+
\`\`\``
|
|
3209
|
+
};
|
|
3185
3210
|
|
|
3186
|
-
|
|
3211
|
+
// src/commands/skill/sections/create-options.ts
|
|
3212
|
+
var createOptions = {
|
|
3213
|
+
title: "Create Options",
|
|
3214
|
+
render: (_ctx) => `### Create Options
|
|
3187
3215
|
|
|
3188
3216
|
| Option | Description | Default |
|
|
3189
3217
|
|--------|-------------|---------|
|
|
@@ -3197,43 +3225,51 @@ cat src/auth/*.ts | ${cmd2} create "Auth System" \\
|
|
|
3197
3225
|
| \`-f, --file <paths...>\` | Files to upload (PDF, PPTX, images, docs) | - |
|
|
3198
3226
|
| \`-l, --language <lang>\` | Output language | en |
|
|
3199
3227
|
| \`-b, --brand <id>\` | Branding ID to apply | - |
|
|
3200
|
-
| \`-o, --output <format>\` | Output: \`human\`, \`json\`, \`quiet\` | human
|
|
3228
|
+
| \`-o, --output <format>\` | Output: \`human\`, \`json\`, \`quiet\` | human |`
|
|
3229
|
+
};
|
|
3201
3230
|
|
|
3202
|
-
|
|
3231
|
+
// src/commands/skill/sections/other-commands.ts
|
|
3232
|
+
var otherCommands = {
|
|
3233
|
+
title: "Other Commands",
|
|
3234
|
+
render: (ctx) => `### Other Commands
|
|
3203
3235
|
|
|
3204
3236
|
\`\`\`bash
|
|
3205
3237
|
# Check authentication
|
|
3206
|
-
${
|
|
3238
|
+
${ctx.cmd} whoami
|
|
3207
3239
|
|
|
3208
3240
|
# List presentations
|
|
3209
|
-
${
|
|
3210
|
-
${
|
|
3241
|
+
${ctx.cmd} list
|
|
3242
|
+
${ctx.cmd} list --format json
|
|
3211
3243
|
|
|
3212
3244
|
# Get presentation details
|
|
3213
|
-
${
|
|
3245
|
+
${ctx.cmd} get <id-or-slug>
|
|
3214
3246
|
|
|
3215
3247
|
# Export to ZIP
|
|
3216
|
-
${
|
|
3248
|
+
${ctx.cmd} export <id-or-slug> -o presentation.zip
|
|
3217
3249
|
|
|
3218
3250
|
# Import presentation
|
|
3219
|
-
${
|
|
3251
|
+
${ctx.cmd} import ./presentation.zip
|
|
3220
3252
|
|
|
3221
3253
|
# Manage branding
|
|
3222
|
-
${
|
|
3223
|
-
${
|
|
3254
|
+
${ctx.cmd} branding list
|
|
3255
|
+
${ctx.cmd} branding extract https://company.com
|
|
3224
3256
|
|
|
3225
3257
|
# Install/manage this skill
|
|
3226
|
-
${
|
|
3227
|
-
${
|
|
3228
|
-
|
|
3258
|
+
${ctx.cmd} skill install
|
|
3259
|
+
${ctx.cmd} skill show
|
|
3260
|
+
\`\`\``
|
|
3261
|
+
};
|
|
3229
3262
|
|
|
3230
|
-
|
|
3263
|
+
// src/commands/skill/sections/examples.ts
|
|
3264
|
+
var examples = {
|
|
3265
|
+
title: "Examples",
|
|
3266
|
+
render: (ctx) => `## Examples
|
|
3231
3267
|
|
|
3232
3268
|
### Present a Codebase Feature
|
|
3233
3269
|
|
|
3234
3270
|
\`\`\`bash
|
|
3235
3271
|
# Read the relevant files and create presentation
|
|
3236
|
-
cat src/lib/auth.ts src/lib/session.ts | ${
|
|
3272
|
+
cat src/lib/auth.ts src/lib/session.ts | ${ctx.cmd} create "Authentication System" \\
|
|
3237
3273
|
--slides 8 --tone educational --audience "New developers" \\
|
|
3238
3274
|
--goal train
|
|
3239
3275
|
\`\`\`
|
|
@@ -3241,7 +3277,7 @@ cat src/lib/auth.ts src/lib/session.ts | ${cmd2} create "Authentication System"
|
|
|
3241
3277
|
### Technical Documentation with Diagrams
|
|
3242
3278
|
|
|
3243
3279
|
\`\`\`bash
|
|
3244
|
-
${
|
|
3280
|
+
${ctx.cmd} create "API Reference" \\
|
|
3245
3281
|
--file ./docs/api.md \\
|
|
3246
3282
|
--file ./diagrams/architecture.png \\
|
|
3247
3283
|
--mode best --amount detailed \\
|
|
@@ -3251,14 +3287,14 @@ ${cmd2} create "API Reference" \\
|
|
|
3251
3287
|
### Quick Project Overview
|
|
3252
3288
|
|
|
3253
3289
|
\`\`\`bash
|
|
3254
|
-
cat README.md package.json | ${
|
|
3290
|
+
cat README.md package.json | ${ctx.cmd} create "Project Introduction" \\
|
|
3255
3291
|
-m instant --slides 5
|
|
3256
3292
|
\`\`\`
|
|
3257
3293
|
|
|
3258
3294
|
### Sales Deck from Existing Presentation
|
|
3259
3295
|
|
|
3260
3296
|
\`\`\`bash
|
|
3261
|
-
${
|
|
3297
|
+
${ctx.cmd} create "Product Demo" \\
|
|
3262
3298
|
--file ./existing-deck.pptx \\
|
|
3263
3299
|
--goal persuade \\
|
|
3264
3300
|
--audience "Enterprise buyers" \\
|
|
@@ -3268,14 +3304,18 @@ ${cmd2} create "Product Demo" \\
|
|
|
3268
3304
|
### Research Presentation
|
|
3269
3305
|
|
|
3270
3306
|
\`\`\`bash
|
|
3271
|
-
${
|
|
3307
|
+
${ctx.cmd} create "Market Analysis" \\
|
|
3272
3308
|
--file ./research.pdf \\
|
|
3273
3309
|
--sources https://report.com/industry.pdf \\
|
|
3274
3310
|
--tone formal --audience "Executive team" \\
|
|
3275
3311
|
--goal report
|
|
3276
|
-
|
|
3312
|
+
\`\`\``
|
|
3313
|
+
};
|
|
3277
3314
|
|
|
3278
|
-
|
|
3315
|
+
// src/commands/skill/sections/output.ts
|
|
3316
|
+
var output = {
|
|
3317
|
+
title: "Output",
|
|
3318
|
+
render: (ctx) => `## Output
|
|
3279
3319
|
|
|
3280
3320
|
Successful creation returns:
|
|
3281
3321
|
\`\`\`
|
|
@@ -3285,44 +3325,168 @@ Successful creation returns:
|
|
|
3285
3325
|
Slides: 8
|
|
3286
3326
|
Generated in: 45s \xB7 12,500 tokens
|
|
3287
3327
|
|
|
3288
|
-
Open: ${url}/en/view/presentations/auth-system-v1-abc123
|
|
3328
|
+
Open: ${ctx.url}/en/view/presentations/auth-system-v1-abc123
|
|
3289
3329
|
\`\`\`
|
|
3290
3330
|
|
|
3291
3331
|
For scripting, use JSON output:
|
|
3292
3332
|
\`\`\`bash
|
|
3293
|
-
URL=$(${
|
|
3294
|
-
|
|
3333
|
+
URL=$(${ctx.cmd} create "Demo" --context "..." -o json | jq -r '.viewUrl')
|
|
3334
|
+
\`\`\``
|
|
3335
|
+
};
|
|
3295
3336
|
|
|
3296
|
-
|
|
3337
|
+
// src/commands/skill/sections/best-practices.ts
|
|
3338
|
+
var bestPractices = {
|
|
3339
|
+
title: "Best Practices",
|
|
3340
|
+
render: (_ctx) => `## Best Practices
|
|
3297
3341
|
|
|
3298
3342
|
1. **Provide rich context** - More context = better slides. Include code, docs, data.
|
|
3299
3343
|
2. **Use file uploads for binary content** - PDFs, images, PPTX files need \`--file\`.
|
|
3300
3344
|
3. **Specify a goal** - Helps tailor the presentation structure and messaging.
|
|
3301
3345
|
4. **Use appropriate mode** - \`instant\` for quick drafts, \`best\` for important presentations.
|
|
3302
3346
|
5. **Specify audience** - Helps tailor complexity and terminology.
|
|
3303
|
-
6. **Combine sources** - Pipe multiple files for comprehensive presentations
|
|
3347
|
+
6. **Combine sources** - Pipe multiple files for comprehensive presentations.`
|
|
3348
|
+
};
|
|
3304
3349
|
|
|
3305
|
-
|
|
3350
|
+
// src/commands/skill/sections/file-types.ts
|
|
3351
|
+
var fileTypes = {
|
|
3352
|
+
title: "Supported File Types",
|
|
3353
|
+
render: (_ctx) => `## Supported File Types
|
|
3306
3354
|
|
|
3307
3355
|
- **Documents**: PDF, DOCX, XLSX, PPTX
|
|
3308
3356
|
- **Images**: JPEG, PNG, GIF, WebP
|
|
3309
|
-
- **Text**: Markdown, TXT, CSV, JSON
|
|
3357
|
+
- **Text**: Markdown, TXT, CSV, JSON`
|
|
3358
|
+
};
|
|
3310
3359
|
|
|
3311
|
-
|
|
3360
|
+
// src/commands/skill/sections/troubleshooting.ts
|
|
3361
|
+
var troubleshooting = {
|
|
3362
|
+
title: "Troubleshooting",
|
|
3363
|
+
render: (ctx) => `## Troubleshooting
|
|
3312
3364
|
|
|
3313
3365
|
\`\`\`bash
|
|
3314
3366
|
# Check if authenticated
|
|
3315
|
-
${
|
|
3367
|
+
${ctx.cmd} whoami
|
|
3316
3368
|
|
|
3317
|
-
#
|
|
3318
|
-
${
|
|
3369
|
+
# Re-authenticate if needed
|
|
3370
|
+
${ctx.cmd} login
|
|
3319
3371
|
|
|
3320
3372
|
# Debug mode
|
|
3321
|
-
${
|
|
3322
|
-
|
|
3323
|
-
|
|
3373
|
+
${ctx.cmd} create "Test" --context "test" --debug
|
|
3374
|
+
\`\`\``
|
|
3375
|
+
};
|
|
3376
|
+
|
|
3377
|
+
// src/commands/skill/sections/video-creation.ts
|
|
3378
|
+
var videoCreation = {
|
|
3379
|
+
title: "Video Creation",
|
|
3380
|
+
render: (ctx) => `## Video Creation
|
|
3381
|
+
|
|
3382
|
+
### How to Create a Perfect Video
|
|
3383
|
+
|
|
3384
|
+
Videos must feel like premium tech launches (Stripe, Apple, Linear) - **Kinetic Composition**, not slideshows.
|
|
3385
|
+
|
|
3386
|
+
**Core Philosophy:** "Nothing sits still. Everything is physics-based. Every pixel breathes."
|
|
3387
|
+
|
|
3388
|
+
### Required Reading (The Motion Bible)
|
|
3389
|
+
|
|
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 |
|
|
3398
|
+
|
|
3399
|
+
**Required:** \`npx skills add https://github.com/remotion-dev/skills --skill remotion-best-practices\`
|
|
3400
|
+
|
|
3401
|
+
### Workflow
|
|
3402
|
+
|
|
3403
|
+
#### Phase 1: Discovery
|
|
3404
|
+
|
|
3405
|
+
Explore the project thoroughly - assets, components, branding, what makes this product/topic unique.
|
|
3406
|
+
|
|
3407
|
+
#### Phase 2: Video Brief
|
|
3408
|
+
|
|
3409
|
+
Present a brief outline (scenes, duration, assets found) and get user approval before production.
|
|
3410
|
+
|
|
3411
|
+
#### Phase 3: Production
|
|
3412
|
+
|
|
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?
|
|
3419
|
+
|
|
3420
|
+
#### Phase 4: Render
|
|
3421
|
+
|
|
3422
|
+
Auto-render when done: \`pnpm exec remotion render FullVideo\`
|
|
3423
|
+
|
|
3424
|
+
Only open Studio if user asks.
|
|
3425
|
+
|
|
3426
|
+
### Kinetic Checklist
|
|
3427
|
+
|
|
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
|
|
3436
|
+
|
|
3437
|
+
### Asset Generation
|
|
3438
|
+
|
|
3439
|
+
\`\`\`bash
|
|
3440
|
+
cat <<EOF | ${ctx.cmd} video create --output ./public
|
|
3441
|
+
{
|
|
3442
|
+
"scenes": [
|
|
3443
|
+
{ "name": "Hook", "script": "..." },
|
|
3444
|
+
{ "name": "Demo", "script": "..." },
|
|
3445
|
+
{ "name": "CTA", "script": "..." }
|
|
3446
|
+
],
|
|
3447
|
+
"voice": "Kore",
|
|
3448
|
+
"musicPrompt": "upbeat corporate"
|
|
3449
|
+
}
|
|
3450
|
+
EOF
|
|
3451
|
+
\`\`\``
|
|
3452
|
+
};
|
|
3453
|
+
|
|
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");
|
|
3324
3481
|
}
|
|
3325
|
-
|
|
3482
|
+
|
|
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 = [
|
|
3326
3490
|
{ name: "Claude Code", dir: ".claude" },
|
|
3327
3491
|
{ name: "Cursor", dir: ".cursor" },
|
|
3328
3492
|
{ name: "Codex", dir: ".codex" },
|
|
@@ -3330,201 +3494,1346 @@ var EDITORS = [
|
|
|
3330
3494
|
{ name: "Windsurf", dir: ".windsurf" },
|
|
3331
3495
|
{ name: "Agent", dir: ".agent" }
|
|
3332
3496
|
];
|
|
3333
|
-
var skillCommand = new Command14("skill").description(`Manage ${brand.displayName} skill for AI coding assistants`).addHelpText(
|
|
3334
|
-
"after",
|
|
3335
|
-
`
|
|
3336
|
-
${chalk12.bold("Examples:")}
|
|
3337
|
-
${chalk12.gray("# Install skill for all detected editors")}
|
|
3338
|
-
$ ${brand.name} skill install
|
|
3339
3497
|
|
|
3340
|
-
|
|
3341
|
-
|
|
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
|
|
3342
3503
|
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
);
|
|
3347
|
-
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) => {
|
|
3348
|
-
const installed = [];
|
|
3349
|
-
const skipped = [];
|
|
3350
|
-
const errors = [];
|
|
3351
|
-
const baseDir = options.local ? process.cwd() : homedir();
|
|
3352
|
-
const skillContent = generateSkillContent(brand);
|
|
3353
|
-
if (options.dir) {
|
|
3354
|
-
const skillPath = join(options.dir, "skills", brand.name);
|
|
3355
|
-
try {
|
|
3356
|
-
installSkill(skillPath, skillContent, options.force);
|
|
3357
|
-
installed.push(options.dir);
|
|
3358
|
-
} catch (err) {
|
|
3359
|
-
errors.push(`${options.dir}: ${err instanceof Error ? err.message : String(err)}`);
|
|
3360
|
-
}
|
|
3361
|
-
} else {
|
|
3362
|
-
for (const editor of EDITORS) {
|
|
3363
|
-
const editorDir = join(baseDir, editor.dir);
|
|
3364
|
-
const skillPath = join(editorDir, "skills", brand.name);
|
|
3365
|
-
const skillFile = join(skillPath, "SKILL.md");
|
|
3366
|
-
if (!existsSync2(editorDir)) {
|
|
3367
|
-
continue;
|
|
3368
|
-
}
|
|
3369
|
-
if (existsSync2(skillFile) && !options.force) {
|
|
3370
|
-
skipped.push(editor.name);
|
|
3371
|
-
continue;
|
|
3372
|
-
}
|
|
3373
|
-
try {
|
|
3374
|
-
installSkill(skillPath, skillContent, options.force);
|
|
3375
|
-
installed.push(editor.name);
|
|
3376
|
-
} catch (err) {
|
|
3377
|
-
errors.push(`${editor.name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
3378
|
-
}
|
|
3379
|
-
}
|
|
3380
|
-
}
|
|
3381
|
-
console.log();
|
|
3382
|
-
if (installed.length > 0) {
|
|
3383
|
-
success("Skill installed successfully");
|
|
3384
|
-
console.log();
|
|
3385
|
-
keyValue("Installed to", installed.join(", "));
|
|
3386
|
-
}
|
|
3387
|
-
if (skipped.length > 0) {
|
|
3388
|
-
console.log();
|
|
3389
|
-
info(`Skipped (already exists): ${skipped.join(", ")}`);
|
|
3390
|
-
console.log(chalk12.gray(" Use --force to overwrite"));
|
|
3391
|
-
}
|
|
3392
|
-
if (errors.length > 0) {
|
|
3393
|
-
console.log();
|
|
3394
|
-
for (const err of errors) {
|
|
3395
|
-
error(err);
|
|
3396
|
-
}
|
|
3397
|
-
}
|
|
3398
|
-
if (installed.length === 0 && skipped.length === 0 && errors.length === 0) {
|
|
3399
|
-
info("No supported AI coding assistants detected.");
|
|
3400
|
-
console.log();
|
|
3401
|
-
console.log(chalk12.gray("Supported editors: " + EDITORS.map((e) => e.name).join(", ")));
|
|
3402
|
-
console.log(chalk12.gray("Use --dir <path> to install to a specific directory"));
|
|
3403
|
-
}
|
|
3404
|
-
console.log();
|
|
3405
|
-
});
|
|
3406
|
-
skillCommand.command("show").description("Display the skill content").action(() => {
|
|
3407
|
-
console.log(generateSkillContent(brand));
|
|
3408
|
-
});
|
|
3409
|
-
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) => {
|
|
3410
|
-
const { rmSync } = await import("fs");
|
|
3411
|
-
const removed = [];
|
|
3412
|
-
const baseDir = options.local ? process.cwd() : homedir();
|
|
3413
|
-
for (const editor of EDITORS) {
|
|
3414
|
-
const skillPath = join(baseDir, editor.dir, "skills", brand.name);
|
|
3415
|
-
if (existsSync2(skillPath)) {
|
|
3416
|
-
try {
|
|
3417
|
-
rmSync(skillPath, { recursive: true });
|
|
3418
|
-
removed.push(editor.name);
|
|
3419
|
-
} catch {
|
|
3420
|
-
}
|
|
3421
|
-
}
|
|
3422
|
-
}
|
|
3423
|
-
console.log();
|
|
3424
|
-
if (removed.length > 0) {
|
|
3425
|
-
success("Skill uninstalled");
|
|
3426
|
-
keyValue("Removed from", removed.join(", "));
|
|
3427
|
-
} else {
|
|
3428
|
-
info("No installed skills found");
|
|
3429
|
-
}
|
|
3430
|
-
console.log();
|
|
3431
|
-
});
|
|
3432
|
-
function installSkill(skillPath, content, force) {
|
|
3433
|
-
const skillFile = join(skillPath, "SKILL.md");
|
|
3434
|
-
mkdirSync(skillPath, { recursive: true });
|
|
3435
|
-
writeFileSync(skillFile, content, "utf-8");
|
|
3436
|
-
}
|
|
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
|
|
3437
3507
|
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
}
|
|
3508
|
+
---
|
|
3509
|
+
|
|
3510
|
+
# The "Kinetic SaaS" Motion Design System
|
|
3511
|
+
|
|
3512
|
+
**Objective:** Replicate the high-energy, fluid feel of premium tech product videos (Stripe, Apple, Linear, Affable.ai).
|
|
3513
|
+
|
|
3514
|
+
**Core Philosophy:** "Nothing sits still. Everything is physics-based. Every pixel breathes."
|
|
3515
|
+
|
|
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.
|
|
3517
|
+
|
|
3518
|
+
---
|
|
3519
|
+
|
|
3520
|
+
## 1. The Global Camera Rig (The "Anti-Static" Layer)
|
|
3521
|
+
|
|
3522
|
+
Even when reading text, the screen is slowly zooming or panning.
|
|
3523
|
+
|
|
3524
|
+
### Virtual Camera
|
|
3525
|
+
|
|
3526
|
+
Every scene must be wrapped in a \`CameraRig\` component:
|
|
3527
|
+
|
|
3528
|
+
\`\`\`tsx
|
|
3529
|
+
const CameraRig: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|
3530
|
+
const frame = useCurrentFrame();
|
|
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]);
|
|
3536
|
+
|
|
3537
|
+
return (
|
|
3538
|
+
<AbsoluteFill style={{
|
|
3539
|
+
transform: \`scale(\${scale}) rotate(\${rotation}deg)\`,
|
|
3540
|
+
}}>
|
|
3541
|
+
{children}
|
|
3542
|
+
</AbsoluteFill>
|
|
3543
|
+
);
|
|
3544
|
+
};
|
|
3545
|
+
\`\`\`
|
|
3546
|
+
|
|
3547
|
+
### Zoom-Through Transitions
|
|
3548
|
+
|
|
3549
|
+
Transitions are NOT just fades - they are camera movements:
|
|
3550
|
+
|
|
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
|
|
3555
|
+
\`\`\`
|
|
3556
|
+
|
|
3557
|
+
---
|
|
3558
|
+
|
|
3559
|
+
## 2. UI & Mockup Animation Rules (Rebuilding Reality)
|
|
3560
|
+
|
|
3561
|
+
UI doesn't just fade in. It pops up with weight.
|
|
3562
|
+
|
|
3563
|
+
### Priority: Use Real Project Components
|
|
3564
|
+
|
|
3565
|
+
**If you're in a project folder, ALWAYS check for actual components first:**
|
|
3566
|
+
|
|
3567
|
+
\`\`\`bash
|
|
3568
|
+
# Before building ANY UI, explore the project
|
|
3569
|
+
ls src/components/
|
|
3570
|
+
ls src/app/
|
|
3571
|
+
\`\`\`
|
|
3572
|
+
|
|
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
|
|
3579
|
+
|
|
3580
|
+
See [project-based.md](project-based.md) for the full component extraction process.
|
|
3581
|
+
|
|
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)
|
|
3586
|
+
|
|
3587
|
+
### The "Pop-Up" Entrance (2.5D Rotation)
|
|
3588
|
+
|
|
3589
|
+
\`\`\`tsx
|
|
3590
|
+
// Heavy spring for weighty feel
|
|
3591
|
+
const progress = spring({
|
|
3592
|
+
frame,
|
|
3593
|
+
fps,
|
|
3594
|
+
config: { mass: 2, damping: 20, stiffness: 100 },
|
|
3526
3595
|
});
|
|
3527
|
-
|
|
3596
|
+
|
|
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]);
|
|
3601
|
+
|
|
3602
|
+
<div style={{
|
|
3603
|
+
transform: \`perspective(1000px) rotateX(\${rotateX}deg) translateY(\${y}px) scale(\${scale})\`,
|
|
3604
|
+
}}>
|
|
3605
|
+
{uiComponent}
|
|
3606
|
+
</div>
|
|
3607
|
+
\`\`\`
|
|
3608
|
+
|
|
3609
|
+
### Cursor Simulation
|
|
3610
|
+
|
|
3611
|
+
**Movement:** Never linear. Cursors move in **Bezier Curves** with an arc.
|
|
3612
|
+
|
|
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
|
+
});
|
|
3620
|
+
|
|
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]);
|
|
3624
|
+
|
|
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;
|
|
3628
|
+
|
|
3629
|
+
// 4. Apply arc to Y position
|
|
3630
|
+
const cursorY = linearY - arcOffset;
|
|
3631
|
+
\`\`\`
|
|
3632
|
+
|
|
3633
|
+
**Click Interaction:**
|
|
3634
|
+
\`\`\`tsx
|
|
3635
|
+
// On click:
|
|
3636
|
+
// 1. Cursor scales down
|
|
3637
|
+
const cursorScale = isClicking ? 0.8 : 1;
|
|
3638
|
+
|
|
3639
|
+
// 2. Button squishes
|
|
3640
|
+
const buttonScaleX = isClicking ? 1.05 : 1;
|
|
3641
|
+
const buttonScaleY = isClicking ? 0.95 : 1;
|
|
3642
|
+
|
|
3643
|
+
// 3. Release both with spring
|
|
3644
|
+
\`\`\`
|
|
3645
|
+
|
|
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>
|
|
3657
|
+
\`\`\`
|
|
3658
|
+
|
|
3659
|
+
### Staggered Lists & Grids
|
|
3660
|
+
|
|
3661
|
+
**Rule:** NEVER show a list or grid all at once.
|
|
3662
|
+
|
|
3663
|
+
\`\`\`tsx
|
|
3664
|
+
const STAGGER_FRAMES = 4; // ~0.05s at 60fps
|
|
3665
|
+
|
|
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
|
+
});
|
|
3673
|
+
|
|
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
|
+
})}
|
|
3683
|
+
\`\`\`
|
|
3684
|
+
|
|
3685
|
+
---
|
|
3686
|
+
|
|
3687
|
+
## 3. Kinetic Typography (Text that Hits)
|
|
3688
|
+
|
|
3689
|
+
Text doesn't fade. It slams in, changes fill, or slides up.
|
|
3690
|
+
|
|
3691
|
+
### Pattern A: The "Masked Reveal"
|
|
3692
|
+
|
|
3693
|
+
Text rises from a floor:
|
|
3694
|
+
|
|
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>
|
|
3703
|
+
\`\`\`
|
|
3704
|
+
|
|
3705
|
+
### Pattern B: Variable Weight (Animate Keywords)
|
|
3706
|
+
|
|
3707
|
+
Don't animate the whole sentence. Animate **keywords**:
|
|
3708
|
+
|
|
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>
|
|
3721
|
+
\`\`\`
|
|
3722
|
+
|
|
3723
|
+
### Pattern C: The "Glitch/Tech" Accent
|
|
3724
|
+
|
|
3725
|
+
Chromatic aberration on impact:
|
|
3726
|
+
|
|
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
|
+
)}
|
|
3735
|
+
\`\`\`
|
|
3736
|
+
|
|
3737
|
+
### Text Colors
|
|
3738
|
+
|
|
3739
|
+
Never pure white (\`#FFF\`). Use \`#F0F0F0\` with subtle gradient or shadow for depth.
|
|
3740
|
+
|
|
3741
|
+
---
|
|
3742
|
+
|
|
3743
|
+
## 4. Atmosphere & Backgrounds (The "Deep Space")
|
|
3744
|
+
|
|
3745
|
+
Background is never a solid color. It's deep black with floating colored orbs.
|
|
3746
|
+
|
|
3747
|
+
### The "Orb" System
|
|
3748
|
+
|
|
3749
|
+
\`\`\`tsx
|
|
3750
|
+
const MovingBackground: React.FC = () => {
|
|
3751
|
+
const frame = useCurrentFrame();
|
|
3752
|
+
|
|
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;
|
|
3758
|
+
|
|
3759
|
+
return (
|
|
3760
|
+
<AbsoluteFill style={{ backgroundColor: '#0a0a0a' }}>
|
|
3761
|
+
{/* Orb 1 - Teal */}
|
|
3762
|
+
<div style={{
|
|
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
|
+
}} />
|
|
3786
|
+
</AbsoluteFill>
|
|
3787
|
+
);
|
|
3788
|
+
};
|
|
3789
|
+
\`\`\`
|
|
3790
|
+
|
|
3791
|
+
### Vignette & Noise
|
|
3792
|
+
|
|
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
|
+
\`\`\`
|
|
3806
|
+
|
|
3807
|
+
---
|
|
3808
|
+
|
|
3809
|
+
## 5. Physics & Timing Reference
|
|
3810
|
+
|
|
3811
|
+
### Spring Configs
|
|
3812
|
+
|
|
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 |
|
|
3818
|
+
|
|
3819
|
+
### Timing Values
|
|
3820
|
+
|
|
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.
|
|
3831
|
+
|
|
3832
|
+
---
|
|
3833
|
+
|
|
3834
|
+
## 6. Micro-Tricks from Premium Videos
|
|
3835
|
+
|
|
3836
|
+
### The "Match Cut"
|
|
3837
|
+
|
|
3838
|
+
Small circle zooms to fill screen, becomes next scene's background:
|
|
3839
|
+
|
|
3840
|
+
\`\`\`tsx
|
|
3841
|
+
// Circle element scales 50x to wipe to next scene
|
|
3842
|
+
const circleScale = interpolate(frame, [MATCH_START, MATCH_END], [1, 50]);
|
|
3843
|
+
\`\`\`
|
|
3844
|
+
|
|
3845
|
+
### Search Bar Typing
|
|
3846
|
+
|
|
3847
|
+
Include blinking cursor:
|
|
3848
|
+
|
|
3849
|
+
\`\`\`tsx
|
|
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
|
+
};
|
|
3862
|
+
\`\`\`
|
|
3863
|
+
|
|
3864
|
+
### The "Card Fan"
|
|
3865
|
+
|
|
3866
|
+
Stack cards, then fan out:
|
|
3867
|
+
|
|
3868
|
+
\`\`\`tsx
|
|
3869
|
+
const cards = [
|
|
3870
|
+
{ rotation: -5, x: -20 },
|
|
3871
|
+
{ rotation: 0, x: 0 },
|
|
3872
|
+
{ rotation: 5, x: 20 },
|
|
3873
|
+
];
|
|
3874
|
+
|
|
3875
|
+
{cards.map((card, i) => {
|
|
3876
|
+
const fanProgress = spring({ frame: frame - 30, fps, config: { damping: 15, stiffness: 100 } });
|
|
3877
|
+
|
|
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
|
+
\`\`\`
|
|
3889
|
+
|
|
3890
|
+
---
|
|
3891
|
+
|
|
3892
|
+
## 7. Strict Animation Guidelines (DO NOT DEVIATE)
|
|
3893
|
+
|
|
3894
|
+
**ROLE:** Senior Motion Graphics Engineer for Remotion.
|
|
3895
|
+
**REFERENCE STYLE:** "High-End SaaS Product Launch" (e.g., Affable.ai, Linear.app).
|
|
3896
|
+
|
|
3897
|
+
### Physics & Timing
|
|
3898
|
+
|
|
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.
|
|
3903
|
+
|
|
3904
|
+
### UI Component Behavior
|
|
3905
|
+
|
|
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
|
|
3914
|
+
|
|
3915
|
+
### Typography Patterns
|
|
3916
|
+
|
|
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.
|
|
3920
|
+
|
|
3921
|
+
### Background "Aliveness"
|
|
3922
|
+
|
|
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.
|
|
3926
|
+
|
|
3927
|
+
---
|
|
3928
|
+
|
|
3929
|
+
## 8. Seamless Scene Transitions (No Hard Cuts)
|
|
3930
|
+
|
|
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.
|
|
3932
|
+
|
|
3933
|
+
### The "Wipe" Rule (No Hard Cuts)
|
|
3934
|
+
|
|
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).
|
|
3938
|
+
|
|
3939
|
+
### The "Zoom-Through" Technique (Best for SaaS)
|
|
3940
|
+
|
|
3941
|
+
As Scene A ends, the camera must **accelerate** its zoom:
|
|
3942
|
+
|
|
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
|
+
\`\`\`
|
|
3951
|
+
|
|
3952
|
+
Result: It looks like the camera flew *through* Scene A to find Scene B behind it.
|
|
3953
|
+
|
|
3954
|
+
### The "Color Swipe" (Safety Net)
|
|
3955
|
+
|
|
3956
|
+
If a complex match-cut is too hard, use a "Curtain":
|
|
3957
|
+
|
|
3958
|
+
\`\`\`tsx
|
|
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]);
|
|
3962
|
+
\`\`\`
|
|
3963
|
+
|
|
3964
|
+
### TransitionWrapper Component
|
|
3965
|
+
|
|
3966
|
+
Use the \`TransitionWrapper\` from shared components:
|
|
3967
|
+
|
|
3968
|
+
\`\`\`tsx
|
|
3969
|
+
import { TransitionWrapper } from './shared';
|
|
3970
|
+
|
|
3971
|
+
// Types: 'slide' | 'zoom' | 'fade' | 'none'
|
|
3972
|
+
// enterFrom: 'left' | 'right' | 'bottom'
|
|
3973
|
+
|
|
3974
|
+
<TransitionWrapper type="slide" enterFrom="bottom">
|
|
3975
|
+
<YourScene />
|
|
3976
|
+
</TransitionWrapper>
|
|
3977
|
+
\`\`\`
|
|
3978
|
+
|
|
3979
|
+
### Critical: Overlap Your Sequences
|
|
3980
|
+
|
|
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).
|
|
3982
|
+
|
|
3983
|
+
\`\`\`tsx
|
|
3984
|
+
export const MyVideo = () => {
|
|
3985
|
+
return (
|
|
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>
|
|
4007
|
+
);
|
|
4008
|
+
};
|
|
4009
|
+
\`\`\`
|
|
4010
|
+
|
|
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."
|
|
4012
|
+
|
|
4013
|
+
---
|
|
4014
|
+
|
|
4015
|
+
## Quick Tips from Motion Devs
|
|
4016
|
+
|
|
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.
|
|
4020
|
+
|
|
4021
|
+
---
|
|
4022
|
+
|
|
4023
|
+
## Self-Check: Is It Kinetic?
|
|
4024
|
+
|
|
4025
|
+
Before rendering, verify:
|
|
4026
|
+
|
|
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
|
|
4037
|
+
|
|
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
|
|
4044
|
+
|
|
4045
|
+
If your video looks like any of these, START OVER.
|
|
4046
|
+
|
|
4047
|
+
## FAILURE 1: Slideshow
|
|
4048
|
+
|
|
4049
|
+
\`\`\`
|
|
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.
|
|
4058
|
+
\`\`\`
|
|
4059
|
+
|
|
4060
|
+
## FAILURE 2: Lorem Ipsum / Placeholder Content
|
|
4061
|
+
|
|
4062
|
+
\`\`\`
|
|
4063
|
+
What you made:
|
|
4064
|
+
- "Lorem ipsum dolor sit amet..."
|
|
4065
|
+
- "Sample text here"
|
|
4066
|
+
- Generic placeholder content
|
|
4067
|
+
|
|
4068
|
+
Use REAL content from the project. REJECTED.
|
|
4069
|
+
\`\`\`
|
|
4070
|
+
|
|
4071
|
+
## FAILURE 3: Static UI Screenshot
|
|
4072
|
+
|
|
4073
|
+
\`\`\`
|
|
4074
|
+
What you made:
|
|
4075
|
+
- Screenshot of the app
|
|
4076
|
+
- Text overlay saying "Our Dashboard"
|
|
4077
|
+
- No motion, no interaction
|
|
4078
|
+
|
|
4079
|
+
Recreate the UI in code and ANIMATE it. REJECTED.
|
|
4080
|
+
\`\`\`
|
|
4081
|
+
|
|
4082
|
+
## FAILURE 4: Elements Just Appearing
|
|
4083
|
+
|
|
4084
|
+
\`\`\`
|
|
4085
|
+
What you made:
|
|
4086
|
+
- Frame 0: nothing
|
|
4087
|
+
- Frame 1: element is fully visible
|
|
4088
|
+
- No transition, no animation
|
|
4089
|
+
|
|
4090
|
+
Every element must animate in with spring/easing. REJECTED.
|
|
4091
|
+
\`\`\`
|
|
4092
|
+
|
|
4093
|
+
## FAILURE 5: Hard Cuts Between Scenes (The "Light Switch" Effect)
|
|
4094
|
+
|
|
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
|
+
\`\`\`
|
|
4106
|
+
|
|
4107
|
+
## FAILURE 6: Static Camera
|
|
4108
|
+
|
|
4109
|
+
\`\`\`
|
|
4110
|
+
What you made:
|
|
4111
|
+
- Camera never moves
|
|
4112
|
+
- No drift, no zoom, no rotation
|
|
4113
|
+
- Feels like watching a screenshot
|
|
4114
|
+
|
|
4115
|
+
Camera must ALWAYS be subtly moving. REJECTED.
|
|
4116
|
+
\`\`\`
|
|
4117
|
+
|
|
4118
|
+
## FAILURE 7: Linear Cursor Movement
|
|
4119
|
+
|
|
4120
|
+
\`\`\`
|
|
4121
|
+
What you made:
|
|
4122
|
+
- Cursor moves in straight lines
|
|
4123
|
+
- No overshoot on stops
|
|
4124
|
+
- Click has no feedback
|
|
4125
|
+
|
|
4126
|
+
Cursor must move in Bezier curves with overshoot. REJECTED.
|
|
4127
|
+
\`\`\`
|
|
4128
|
+
`
|
|
4129
|
+
},
|
|
4130
|
+
{
|
|
4131
|
+
filename: "parameterization.md",
|
|
4132
|
+
content: `# Parameterization (Critical)
|
|
4133
|
+
|
|
4134
|
+
Never hardcode frame numbers. Use variables for all keyframes.
|
|
4135
|
+
|
|
4136
|
+
## Why
|
|
4137
|
+
|
|
4138
|
+
When you change timing early in video, everything else breaks if hardcoded.
|
|
4139
|
+
|
|
4140
|
+
## Pattern
|
|
4141
|
+
|
|
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;
|
|
4150
|
+
|
|
4151
|
+
// Use in animations
|
|
4152
|
+
opacity: interpolate(frame, [TITLE_IN, TITLE_IN + 15], [0, 1])
|
|
4153
|
+
\`\`\`
|
|
4154
|
+
|
|
4155
|
+
## Scene Duration
|
|
4156
|
+
|
|
4157
|
+
\`\`\`ts
|
|
4158
|
+
// Calculate from keyframes, don't hardcode
|
|
4159
|
+
const sceneDuration = SCENE_END - SCENE_START;
|
|
4160
|
+
\`\`\`
|
|
4161
|
+
|
|
4162
|
+
## Audio Sync
|
|
4163
|
+
|
|
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
|
+
\`\`\`
|
|
4169
|
+
|
|
4170
|
+
## Multi-Scene
|
|
4171
|
+
|
|
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
|
|
4183
|
+
|
|
4184
|
+
Explicit z-index prevents visual bugs.
|
|
4185
|
+
|
|
4186
|
+
## Z-Index Scale
|
|
4187
|
+
|
|
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) |
|
|
4198
|
+
|
|
4199
|
+
## Composition Structure
|
|
4200
|
+
|
|
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
|
+
\`\`\`
|
|
4224
|
+
|
|
4225
|
+
## Overlapping Sequences
|
|
4226
|
+
|
|
4227
|
+
For zoom-through transitions, scenes overlap:
|
|
4228
|
+
|
|
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)
|
|
4242
|
+
|
|
4243
|
+
When creating a video for a project, **copy the actual components** - don't rebuild from scratch.
|
|
4244
|
+
|
|
4245
|
+
## The Goal
|
|
4246
|
+
|
|
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).
|
|
4248
|
+
|
|
4249
|
+
## Process: Eject \u2192 Simplify \u2192 Animate
|
|
4250
|
+
|
|
4251
|
+
### Step 1: Find Components to Feature
|
|
4252
|
+
|
|
4253
|
+
\`\`\`bash
|
|
4254
|
+
# Explore the project structure
|
|
4255
|
+
ls src/components/
|
|
4256
|
+
ls src/app/
|
|
4257
|
+
# Find the key UI: dashboards, forms, cards, modals, etc.
|
|
4258
|
+
\`\`\`
|
|
4259
|
+
|
|
4260
|
+
### Step 2: Copy Component Code
|
|
4261
|
+
|
|
4262
|
+
Copy the actual component file into your Remotion project.
|
|
4263
|
+
|
|
4264
|
+
### Step 3: Eject - Strip Logic, Keep Visuals
|
|
4265
|
+
|
|
4266
|
+
\`\`\`tsx
|
|
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
|
+
}
|
|
4273
|
+
|
|
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
|
|
4278
|
+
|
|
4279
|
+
const entranceProgress = spring({ frame: progress, fps, config: { mass: 2, damping: 20, stiffness: 100 } });
|
|
4280
|
+
const rotateX = interpolate(entranceProgress, [0, 1], [20, 0]);
|
|
4281
|
+
|
|
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
|
+
}
|
|
4291
|
+
\`\`\`
|
|
4292
|
+
|
|
4293
|
+
### Step 4: Add Kinetic Animation
|
|
4294
|
+
|
|
4295
|
+
Apply the rules from kinetic-saas.md:
|
|
4296
|
+
- 2.5D rotation entrance
|
|
4297
|
+
- Spring physics
|
|
4298
|
+
- Staggered children
|
|
4299
|
+
- Cursor interaction states
|
|
4300
|
+
|
|
4301
|
+
### Step 5: Fill With Real Demo Data
|
|
4302
|
+
|
|
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"
|
|
4310
|
+
\`\`\`
|
|
4311
|
+
|
|
4312
|
+
## What to Copy
|
|
4313
|
+
|
|
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 |
|
|
4320
|
+
|
|
4321
|
+
## Common Mistakes
|
|
4322
|
+
|
|
4323
|
+
\u274C Using placeholder colors like \`#333\`
|
|
4324
|
+
\u2705 Using exact project colors from tailwind config
|
|
4325
|
+
|
|
4326
|
+
\u274C Generic content: "Feature 1", "Lorem ipsum"
|
|
4327
|
+
\u2705 Real content: "Unlimited projects", "Priority support"
|
|
4328
|
+
|
|
4329
|
+
\u274C Building from scratch based on screenshots
|
|
4330
|
+
\u2705 Copying actual component code and ejecting it
|
|
4331
|
+
|
|
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)
|
|
4339
|
+
|
|
4340
|
+
**Platforms:** TikTok, Instagram Reels, YouTube Shorts (all 9:16 vertical)
|
|
4341
|
+
|
|
4342
|
+
**Philosophy:** "Don't make ads. Make TikToks." Your video must look native to the platform - lo-fi beats polished.
|
|
4343
|
+
|
|
4344
|
+
---
|
|
4345
|
+
|
|
4346
|
+
## 1. The "3-Second War"
|
|
4347
|
+
|
|
4348
|
+
You have 3 seconds before the thumb scrolls. Win or lose everything.
|
|
4349
|
+
|
|
4350
|
+
### Hook Requirements (0-3s)
|
|
4351
|
+
|
|
4352
|
+
- **Text on screen in FRAME 1** - no waiting for audio
|
|
4353
|
+
- **Movement** - never start static
|
|
4354
|
+
- **Pattern interrupt** - something unexpected
|
|
4355
|
+
|
|
4356
|
+
### Hook Types
|
|
4357
|
+
|
|
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 |
|
|
4364
|
+
|
|
4365
|
+
### Audio Hooks
|
|
4366
|
+
|
|
4367
|
+
Bad: "Today I want to talk about..."
|
|
4368
|
+
Good: "Stop scrolling if you hate [X]." / "This feels illegal to know."
|
|
4369
|
+
|
|
4370
|
+
---
|
|
4371
|
+
|
|
4372
|
+
## 2. Pacing Rules (No Dead Air)
|
|
4373
|
+
|
|
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 |
|
|
4380
|
+
|
|
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";
|
|
4396
|
+
\`\`\`
|
|
4397
|
+
|
|
4398
|
+
---
|
|
4399
|
+
|
|
4400
|
+
## 4. Safe Zone Rules (CRITICAL)
|
|
4401
|
+
|
|
4402
|
+
Platform UI overlays your content. Plan for it.
|
|
4403
|
+
|
|
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
|
|
4420
|
+
|
|
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
|
+
};
|
|
4428
|
+
|
|
4429
|
+
// Captions must sit ABOVE bottom safe zone
|
|
4430
|
+
const CAPTION_BOTTOM = 350; // px from bottom
|
|
4431
|
+
\`\`\`
|
|
4432
|
+
|
|
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
|
+
\`\`\`
|
|
4456
|
+
|
|
4457
|
+
---
|
|
4458
|
+
|
|
4459
|
+
## 5. Content Formats
|
|
4460
|
+
|
|
4461
|
+
### Green Screen (Speaker + Background)
|
|
4462
|
+
|
|
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>
|
|
4473
|
+
\`\`\`
|
|
4474
|
+
|
|
4475
|
+
### Listicle (Rapid-Fire)
|
|
4476
|
+
|
|
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
|
+
\`\`\`
|
|
4482
|
+
|
|
4483
|
+
### Visual ASMR (No Talking)
|
|
4484
|
+
|
|
4485
|
+
- No voiceover, music only
|
|
4486
|
+
- Slow, deliberate cursor movements
|
|
4487
|
+
- Satisfying click feedback
|
|
4488
|
+
- Ken Burns zoom on details
|
|
4489
|
+
|
|
4490
|
+
### POV Skit
|
|
4491
|
+
|
|
4492
|
+
\`\`\`tsx
|
|
4493
|
+
<div style={{ position: "absolute", top: 160, left: 40, right: 120, fontSize: 42 }}>
|
|
4494
|
+
POV: {setup}
|
|
4495
|
+
</div>
|
|
4496
|
+
\`\`\`
|
|
4497
|
+
|
|
4498
|
+
---
|
|
4499
|
+
|
|
4500
|
+
## 6. TikTok Captions
|
|
4501
|
+
|
|
4502
|
+
Use \`@remotion/captions\` with \`createTikTokStyleCaptions\`:
|
|
4503
|
+
|
|
4504
|
+
\`\`\`tsx
|
|
4505
|
+
import { createTikTokStyleCaptions } from "@remotion/captions";
|
|
4506
|
+
|
|
4507
|
+
const { pages } = createTikTokStyleCaptions({
|
|
4508
|
+
captions: transcriptCaptions,
|
|
4509
|
+
combineTokensWithinMilliseconds: 1200, // Higher = more words per page
|
|
4510
|
+
});
|
|
4511
|
+
\`\`\`
|
|
4512
|
+
|
|
4513
|
+
### Word-by-Word Highlighting
|
|
4514
|
+
|
|
4515
|
+
\`\`\`tsx
|
|
4516
|
+
{page.tokens.map((token) => {
|
|
4517
|
+
const isActive = currentTimeMs >= token.fromMs && currentTimeMs < token.toMs;
|
|
4518
|
+
return (
|
|
4519
|
+
<span style={{
|
|
4520
|
+
color: isActive ? "#FFD700" : "#fff",
|
|
4521
|
+
fontSize: 64,
|
|
4522
|
+
fontWeight: 900,
|
|
4523
|
+
textTransform: "uppercase",
|
|
4524
|
+
}}>
|
|
4525
|
+
{token.text}
|
|
4526
|
+
</span>
|
|
4527
|
+
);
|
|
4528
|
+
})}
|
|
4529
|
+
\`\`\`
|
|
4530
|
+
|
|
4531
|
+
---
|
|
4532
|
+
|
|
4533
|
+
## 7. Script Structure (60 Seconds)
|
|
4534
|
+
|
|
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) |
|
|
4542
|
+
|
|
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" |
|
|
4550
|
+
|
|
4551
|
+
---
|
|
4552
|
+
|
|
4553
|
+
## 8. Quick Reference
|
|
4554
|
+
|
|
4555
|
+
### Platform Specs
|
|
4556
|
+
|
|
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 |
|
|
4563
|
+
|
|
4564
|
+
### Timing (30fps)
|
|
4565
|
+
|
|
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
|
|
4583
|
+
`
|
|
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
|
+
};
|
|
4625
|
+
const baseDir = options.local ? process.cwd() : homedir();
|
|
4626
|
+
if (options.dir) {
|
|
4627
|
+
try {
|
|
4628
|
+
const resolvedDir = resolve4(options.dir);
|
|
4629
|
+
const skillPath = validatePath(resolvedDir, join("skills", skillName));
|
|
4630
|
+
installSkillToPath(skillPath, content);
|
|
4631
|
+
result.installed.push(options.dir);
|
|
4632
|
+
} catch (err) {
|
|
4633
|
+
result.errors.push(`${options.dir}: ${err instanceof Error ? err.message : String(err)}`);
|
|
4634
|
+
}
|
|
4635
|
+
} else {
|
|
4636
|
+
for (const editor of SUPPORTED_EDITORS) {
|
|
4637
|
+
const editorDir = join(baseDir, editor.dir);
|
|
4638
|
+
const skillPath = join(editorDir, "skills", skillName);
|
|
4639
|
+
const skillFile = join(skillPath, "SKILL.md");
|
|
4640
|
+
if (!existsSync2(editorDir)) {
|
|
4641
|
+
continue;
|
|
4642
|
+
}
|
|
4643
|
+
if (existsSync2(skillFile) && !options.force) {
|
|
4644
|
+
result.skipped.push(editor.name);
|
|
4645
|
+
continue;
|
|
4646
|
+
}
|
|
4647
|
+
try {
|
|
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);
|
|
4669
|
+
} catch (err) {
|
|
4670
|
+
result.errors.push(`${editor.name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
4671
|
+
}
|
|
4672
|
+
}
|
|
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
|
+
});
|
|
4702
|
+
console.log();
|
|
4703
|
+
if (result.installed.length > 0) {
|
|
4704
|
+
success("Skill installed successfully");
|
|
4705
|
+
console.log();
|
|
4706
|
+
keyValue("Installed to", result.installed.join(", "));
|
|
4707
|
+
}
|
|
4708
|
+
if (result.skipped.length > 0) {
|
|
4709
|
+
console.log();
|
|
4710
|
+
info(`Skipped (already exists): ${result.skipped.join(", ")}`);
|
|
4711
|
+
console.log(chalk12.gray(" Use --force to overwrite"));
|
|
4712
|
+
}
|
|
4713
|
+
if (result.errors.length > 0) {
|
|
4714
|
+
console.log();
|
|
4715
|
+
for (const err of result.errors) {
|
|
4716
|
+
error(err);
|
|
4717
|
+
}
|
|
4718
|
+
}
|
|
4719
|
+
if (result.installed.length === 0 && result.skipped.length === 0 && result.errors.length === 0) {
|
|
4720
|
+
info("No supported AI coding assistants detected.");
|
|
4721
|
+
console.log();
|
|
4722
|
+
console.log(chalk12.gray("Supported editors: " + getSupportedEditorNames().join(", ")));
|
|
4723
|
+
console.log(chalk12.gray("Use --dir <path> to install to a specific directory"));
|
|
4724
|
+
}
|
|
4725
|
+
console.log();
|
|
4726
|
+
});
|
|
4727
|
+
skillCommand.command("show").description("Display the skill content").action(() => {
|
|
4728
|
+
console.log(generateSkillContent(brand));
|
|
4729
|
+
});
|
|
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) => {
|
|
4731
|
+
const result = uninstallSkill(brand.name, { local: options.local });
|
|
4732
|
+
console.log();
|
|
4733
|
+
if (result.removed.length > 0) {
|
|
4734
|
+
success("Skill uninstalled");
|
|
4735
|
+
keyValue("Removed from", result.removed.join(", "));
|
|
4736
|
+
} else {
|
|
4737
|
+
info("No installed skills found");
|
|
4738
|
+
}
|
|
4739
|
+
if (result.errors.length > 0) {
|
|
4740
|
+
for (const err of result.errors) {
|
|
4741
|
+
warn(`Failed to remove: ${err}`);
|
|
4742
|
+
}
|
|
4743
|
+
}
|
|
4744
|
+
console.log();
|
|
4745
|
+
});
|
|
4746
|
+
|
|
4747
|
+
// src/commands/tts.ts
|
|
4748
|
+
init_api();
|
|
4749
|
+
init_output();
|
|
4750
|
+
init_types();
|
|
4751
|
+
import { Command as Command15 } from "commander";
|
|
4752
|
+
import ora8 from "ora";
|
|
4753
|
+
import { writeFile as writeFile2 } from "fs/promises";
|
|
4754
|
+
var generateCommand = new Command15("generate").description("Generate speech from text").requiredOption("-t, --text <text>", "Text to convert to speech").requiredOption("-o, --output <path>", "Output file path").option("-v, --voice <voice>", "Voice name or ID (e.g., Kore, Rachel, alloy)").option("-p, --provider <provider>", "Provider: gemini, elevenlabs, openai").option("-m, --model <model>", "Model (provider-specific)").option("-s, --speed <speed>", "Speech speed 0.25-4.0 (default: 1.0)").option("-f, --format <format>", "Output format: human, json, quiet", "human").action(async (options) => {
|
|
4755
|
+
const format = options.format;
|
|
4756
|
+
const spinner = format === "human" ? ora8("Generating speech...").start() : null;
|
|
4757
|
+
let speed;
|
|
4758
|
+
if (options.speed) {
|
|
4759
|
+
speed = parseFloat(options.speed);
|
|
4760
|
+
if (isNaN(speed) || speed < 0.25 || speed > 4) {
|
|
4761
|
+
spinner?.stop();
|
|
4762
|
+
error("Speed must be between 0.25 and 4.0");
|
|
4763
|
+
process.exit(EXIT_CODES.INVALID_INPUT);
|
|
4764
|
+
}
|
|
4765
|
+
}
|
|
4766
|
+
try {
|
|
4767
|
+
const result = await generateSpeech({
|
|
4768
|
+
text: options.text,
|
|
4769
|
+
options: {
|
|
4770
|
+
provider: options.provider,
|
|
4771
|
+
voice: options.voice,
|
|
4772
|
+
model: options.model,
|
|
4773
|
+
speed
|
|
4774
|
+
}
|
|
4775
|
+
});
|
|
4776
|
+
spinner?.stop();
|
|
4777
|
+
const outputPath = options.output.endsWith(`.${result.format}`) ? options.output : `${options.output}.${result.format}`;
|
|
4778
|
+
await writeFile2(outputPath, result.audioData);
|
|
4779
|
+
if (format === "json") {
|
|
4780
|
+
printJson({
|
|
4781
|
+
status: "completed",
|
|
4782
|
+
output: outputPath,
|
|
4783
|
+
duration: result.duration,
|
|
4784
|
+
cost: result.cost,
|
|
4785
|
+
provider: result.provider,
|
|
4786
|
+
format: result.format
|
|
4787
|
+
});
|
|
4788
|
+
return;
|
|
4789
|
+
}
|
|
4790
|
+
if (format === "quiet") {
|
|
4791
|
+
console.log(outputPath);
|
|
4792
|
+
return;
|
|
4793
|
+
}
|
|
4794
|
+
success(`Saved to: ${outputPath}`);
|
|
4795
|
+
info(`Duration: ${result.duration.toFixed(2)}s`);
|
|
4796
|
+
info(`Provider: ${result.provider}`);
|
|
4797
|
+
info(`Cost: $${result.cost.toFixed(6)}`);
|
|
4798
|
+
} catch (err) {
|
|
4799
|
+
spinner?.stop();
|
|
4800
|
+
error(err instanceof Error ? err.message : "Unknown error");
|
|
4801
|
+
process.exit(EXIT_CODES.GENERAL_ERROR);
|
|
4802
|
+
}
|
|
4803
|
+
});
|
|
4804
|
+
var voicesCommand = new Command15("voices").description("List available voices").option("-p, --provider <provider>", "Filter by provider: gemini, elevenlabs, openai").option("-f, --format <format>", "Output format: human, json", "human").action(async (options) => {
|
|
4805
|
+
const spinner = options.format === "human" ? ora8("Fetching voices...").start() : null;
|
|
4806
|
+
try {
|
|
4807
|
+
const result = await getVoices();
|
|
4808
|
+
spinner?.stop();
|
|
4809
|
+
if (options.format === "json") {
|
|
4810
|
+
if (options.provider) {
|
|
4811
|
+
const providerVoices = result.voices[options.provider];
|
|
4812
|
+
printJson(providerVoices || []);
|
|
4813
|
+
} else {
|
|
4814
|
+
printJson(result.voices);
|
|
4815
|
+
}
|
|
4816
|
+
return;
|
|
4817
|
+
}
|
|
4818
|
+
const providers = options.provider ? [options.provider] : ["gemini", "elevenlabs", "openai"];
|
|
4819
|
+
for (const provider of providers) {
|
|
4820
|
+
const voices = result.voices[provider];
|
|
4821
|
+
if (!voices || voices.length === 0) continue;
|
|
4822
|
+
console.log();
|
|
4823
|
+
console.log(`${provider.toUpperCase()} Voices:`);
|
|
4824
|
+
console.log("-".repeat(50));
|
|
4825
|
+
for (const voice of voices) {
|
|
4826
|
+
console.log(` ${voice.name} (${voice.id})`);
|
|
4827
|
+
console.log(` ${voice.description}`);
|
|
4828
|
+
}
|
|
4829
|
+
}
|
|
4830
|
+
} catch (err) {
|
|
4831
|
+
spinner?.stop();
|
|
4832
|
+
error(err instanceof Error ? err.message : "Unknown error");
|
|
4833
|
+
process.exit(EXIT_CODES.GENERAL_ERROR);
|
|
4834
|
+
}
|
|
4835
|
+
});
|
|
4836
|
+
var ttsCommand = new Command15("tts").description("Text-to-speech commands").addCommand(generateCommand).addCommand(voicesCommand);
|
|
3528
4837
|
|
|
3529
4838
|
// src/commands/music.ts
|
|
3530
4839
|
init_api();
|
|
@@ -3860,8 +5169,118 @@ init_output();
|
|
|
3860
5169
|
init_types();
|
|
3861
5170
|
import { Command as Command19 } from "commander";
|
|
3862
5171
|
import ora12 from "ora";
|
|
3863
|
-
import { mkdir, writeFile as writeFile5, readFile as readFile2 } from "fs/promises";
|
|
3864
|
-
import { join as join2 } from "path";
|
|
5172
|
+
import { mkdir, writeFile as writeFile5, readFile as readFile2, access, rm } from "fs/promises";
|
|
5173
|
+
import { join as join2, resolve as resolve5 } from "path";
|
|
5174
|
+
import { execSync, spawn } from "child_process";
|
|
5175
|
+
var DEFAULT_TEMPLATE = "inizio-inc/remotion-composition";
|
|
5176
|
+
var DEFAULT_FPS = 30;
|
|
5177
|
+
function parseScriptIntoSections(script) {
|
|
5178
|
+
if (script.includes("---") || script.includes("[Section")) {
|
|
5179
|
+
const parts = script.split(/---|\[Section \d+\]/i).filter((s) => s.trim());
|
|
5180
|
+
if (parts.length > 1) {
|
|
5181
|
+
return parts.map((p) => p.trim());
|
|
5182
|
+
}
|
|
5183
|
+
}
|
|
5184
|
+
const sentences = script.split(/(?<=[.!?])\s+/).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
5185
|
+
const sections = [];
|
|
5186
|
+
let pendingShort = "";
|
|
5187
|
+
for (const sentence of sentences) {
|
|
5188
|
+
const wordCount = sentence.split(/\s+/).length;
|
|
5189
|
+
if (pendingShort) {
|
|
5190
|
+
sections.push(`${pendingShort} ${sentence}`);
|
|
5191
|
+
pendingShort = "";
|
|
5192
|
+
} else if (wordCount < 5 && sections.length < sentences.length - 1) {
|
|
5193
|
+
pendingShort = sentence;
|
|
5194
|
+
} else {
|
|
5195
|
+
sections.push(sentence);
|
|
5196
|
+
}
|
|
5197
|
+
}
|
|
5198
|
+
if (pendingShort) {
|
|
5199
|
+
if (sections.length > 0) {
|
|
5200
|
+
sections[sections.length - 1] += ` ${pendingShort}`;
|
|
5201
|
+
} else {
|
|
5202
|
+
sections.push(pendingShort);
|
|
5203
|
+
}
|
|
5204
|
+
}
|
|
5205
|
+
return sections;
|
|
5206
|
+
}
|
|
5207
|
+
function calculateSectionTiming(sections, totalDuration, fps = DEFAULT_FPS, timestamps) {
|
|
5208
|
+
if (timestamps && timestamps.characters.length > 0) {
|
|
5209
|
+
return calculateSectionTimingFromTimestamps(sections, timestamps, fps);
|
|
5210
|
+
}
|
|
5211
|
+
const totalWords = sections.reduce((sum, s) => sum + s.split(/\s+/).length, 0);
|
|
5212
|
+
let currentTime = 0;
|
|
5213
|
+
return sections.map((text, index) => {
|
|
5214
|
+
const wordCount = text.split(/\s+/).length;
|
|
5215
|
+
const proportion = wordCount / totalWords;
|
|
5216
|
+
const durationInSeconds = totalDuration * proportion;
|
|
5217
|
+
const durationInFrames = Math.round(durationInSeconds * fps);
|
|
5218
|
+
const section = {
|
|
5219
|
+
id: index + 1,
|
|
5220
|
+
text,
|
|
5221
|
+
wordCount,
|
|
5222
|
+
startTime: currentTime,
|
|
5223
|
+
endTime: currentTime + durationInSeconds,
|
|
5224
|
+
durationInSeconds,
|
|
5225
|
+
durationInFrames
|
|
5226
|
+
};
|
|
5227
|
+
currentTime += durationInSeconds;
|
|
5228
|
+
return section;
|
|
5229
|
+
});
|
|
5230
|
+
}
|
|
5231
|
+
function calculateSectionTimingFromTimestamps(sections, timestamps, fps) {
|
|
5232
|
+
const { characters, characterStartTimesSeconds, characterEndTimesSeconds } = timestamps;
|
|
5233
|
+
const fullText = characters.join("");
|
|
5234
|
+
const results = [];
|
|
5235
|
+
let charIndex = 0;
|
|
5236
|
+
for (let i = 0; i < sections.length; i++) {
|
|
5237
|
+
const sectionText = sections[i];
|
|
5238
|
+
const sectionLength = sectionText.length;
|
|
5239
|
+
while (charIndex < characters.length && characters[charIndex].match(/^\s*$/)) {
|
|
5240
|
+
charIndex++;
|
|
5241
|
+
}
|
|
5242
|
+
const startCharIndex = charIndex;
|
|
5243
|
+
const startTime = characterStartTimesSeconds[startCharIndex] || 0;
|
|
5244
|
+
charIndex += sectionLength;
|
|
5245
|
+
let endCharIndex = charIndex - 1;
|
|
5246
|
+
while (endCharIndex > startCharIndex && characters[endCharIndex]?.match(/^\s*$/)) {
|
|
5247
|
+
endCharIndex--;
|
|
5248
|
+
}
|
|
5249
|
+
const endTime = characterEndTimesSeconds[Math.min(endCharIndex, characterEndTimesSeconds.length - 1)] || startTime + 1;
|
|
5250
|
+
const durationInSeconds = endTime - startTime;
|
|
5251
|
+
const durationInFrames = Math.round(durationInSeconds * fps);
|
|
5252
|
+
results.push({
|
|
5253
|
+
id: i + 1,
|
|
5254
|
+
text: sectionText,
|
|
5255
|
+
wordCount: sectionText.split(/\s+/).length,
|
|
5256
|
+
startTime,
|
|
5257
|
+
endTime,
|
|
5258
|
+
durationInSeconds,
|
|
5259
|
+
durationInFrames
|
|
5260
|
+
});
|
|
5261
|
+
}
|
|
5262
|
+
return results;
|
|
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
|
+
}
|
|
3865
5284
|
async function downloadFile3(url, outputPath) {
|
|
3866
5285
|
if (url.startsWith("data:")) {
|
|
3867
5286
|
const matches = url.match(/^data:[^;]+;base64,(.+)$/);
|
|
@@ -3891,61 +5310,199 @@ function getExtension(url) {
|
|
|
3891
5310
|
}
|
|
3892
5311
|
return "jpg";
|
|
3893
5312
|
}
|
|
3894
|
-
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) => {
|
|
3895
5314
|
const format = options.format;
|
|
3896
5315
|
const spinner = format === "human" ? ora12("Initializing...").start() : null;
|
|
3897
5316
|
try {
|
|
3898
|
-
|
|
3899
|
-
|
|
5317
|
+
const stdinData = await readStdin2();
|
|
5318
|
+
let scenesInput = null;
|
|
5319
|
+
if (stdinData) {
|
|
3900
5320
|
try {
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
5321
|
+
const parsed = JSON.parse(stdinData);
|
|
5322
|
+
if (parsed.scenes && Array.isArray(parsed.scenes)) {
|
|
5323
|
+
scenesInput = parsed;
|
|
5324
|
+
}
|
|
5325
|
+
} catch {
|
|
3906
5326
|
}
|
|
3907
5327
|
}
|
|
3908
|
-
if (!
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
spinner?.stop();
|
|
3918
|
-
error("Number of images must be between 1 and 20");
|
|
3919
|
-
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
|
+
}
|
|
3920
5337
|
}
|
|
5338
|
+
const voice = scenesInput?.voice || options.voice;
|
|
5339
|
+
const musicPrompt = scenesInput?.musicPrompt || options.musicPrompt || "uplifting background music, positive energy";
|
|
3921
5340
|
const audioDir = join2(options.output, "audio");
|
|
3922
5341
|
const imagesDir = join2(options.output, "images");
|
|
5342
|
+
const videosDir = join2(options.output, "videos");
|
|
3923
5343
|
if (spinner) spinner.text = "Creating directories...";
|
|
3924
5344
|
await mkdir(audioDir, { recursive: true });
|
|
3925
5345
|
await mkdir(imagesDir, { recursive: true });
|
|
5346
|
+
await mkdir(videosDir, { recursive: true });
|
|
3926
5347
|
let totalCost = 0;
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
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
|
+
}
|
|
3946
5504
|
}
|
|
3947
|
-
const musicDuration = Math.min(30, Math.ceil(
|
|
3948
|
-
const musicPrompt = options.musicPrompt || "uplifting background music, positive energy";
|
|
5505
|
+
const musicDuration = Math.min(30, Math.ceil(totalDuration) + 5);
|
|
3949
5506
|
if (spinner) spinner.text = "Generating music...";
|
|
3950
5507
|
let musicResult = await generateMusic({
|
|
3951
5508
|
prompt: musicPrompt,
|
|
@@ -3980,58 +5537,15 @@ var createCommand2 = new Command19("create").description("Create video assets (v
|
|
|
3980
5537
|
success(`Music: ${musicPath} (${musicInfo.duration}s)`);
|
|
3981
5538
|
spinner?.start();
|
|
3982
5539
|
}
|
|
3983
|
-
if (spinner) spinner.text = "Searching for images...";
|
|
3984
|
-
const imageResults = await searchImages({
|
|
3985
|
-
query: topic,
|
|
3986
|
-
options: {
|
|
3987
|
-
maxResults: numImages,
|
|
3988
|
-
size: "large",
|
|
3989
|
-
safeSearch: true
|
|
3990
|
-
}
|
|
3991
|
-
});
|
|
3992
|
-
const allImages = imageResults.data.results.flatMap(
|
|
3993
|
-
(providerResult) => providerResult.results.map((img) => ({
|
|
3994
|
-
...img,
|
|
3995
|
-
provider: providerResult.providerName
|
|
3996
|
-
}))
|
|
3997
|
-
);
|
|
3998
|
-
totalCost += imageResults.data.totalCost;
|
|
3999
|
-
const downloadedImages = [];
|
|
4000
|
-
for (let i = 0; i < Math.min(allImages.length, numImages); i++) {
|
|
4001
|
-
const img = allImages[i];
|
|
4002
|
-
const ext = getExtension(img.url);
|
|
4003
|
-
const filename = `scene-${i + 1}.${ext}`;
|
|
4004
|
-
const imagePath = join2(imagesDir, filename);
|
|
4005
|
-
if (spinner) spinner.text = `Downloading image ${i + 1}/${Math.min(allImages.length, numImages)}...`;
|
|
4006
|
-
try {
|
|
4007
|
-
await downloadFile3(img.url, imagePath);
|
|
4008
|
-
downloadedImages.push({
|
|
4009
|
-
path: `images/${filename}`,
|
|
4010
|
-
url: img.url,
|
|
4011
|
-
width: img.width,
|
|
4012
|
-
height: img.height,
|
|
4013
|
-
query: topic
|
|
4014
|
-
});
|
|
4015
|
-
} catch (err) {
|
|
4016
|
-
if (format === "human") {
|
|
4017
|
-
spinner?.stop();
|
|
4018
|
-
warn(`Failed to download image ${i + 1}: ${err instanceof Error ? err.message : "Unknown error"}`);
|
|
4019
|
-
spinner?.start();
|
|
4020
|
-
}
|
|
4021
|
-
}
|
|
4022
|
-
}
|
|
4023
|
-
if (format === "human") {
|
|
4024
|
-
spinner?.stop();
|
|
4025
|
-
success(`Images: Downloaded ${downloadedImages.length} images to ${imagesDir}`);
|
|
4026
|
-
spinner?.start();
|
|
4027
|
-
}
|
|
4028
5540
|
if (spinner) spinner.text = "Writing manifest...";
|
|
5541
|
+
const totalDurationInFrames = Math.round(totalDuration * DEFAULT_FPS);
|
|
4029
5542
|
const manifest = {
|
|
4030
|
-
topic,
|
|
4031
|
-
script,
|
|
4032
|
-
voiceover: voiceoverInfo,
|
|
4033
5543
|
music: musicInfo,
|
|
4034
|
-
images:
|
|
5544
|
+
images: allImages,
|
|
5545
|
+
videos: allVideos,
|
|
5546
|
+
scenes,
|
|
5547
|
+
totalDurationInFrames,
|
|
5548
|
+
fps: DEFAULT_FPS,
|
|
4035
5549
|
totalCost,
|
|
4036
5550
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4037
5551
|
};
|
|
@@ -4049,18 +5563,19 @@ var createCommand2 = new Command19("create").description("Create video assets (v
|
|
|
4049
5563
|
console.log();
|
|
4050
5564
|
success("Video assets created successfully!");
|
|
4051
5565
|
console.log();
|
|
4052
|
-
info(`
|
|
4053
|
-
|
|
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
|
+
}
|
|
4054
5575
|
info(`Music: ${musicInfo.path} (${musicInfo.duration}s)`);
|
|
4055
|
-
info(`Images: ${downloadedImages.length} downloaded`);
|
|
4056
5576
|
info(`Manifest: ${manifestPath}`);
|
|
4057
5577
|
console.log();
|
|
4058
5578
|
info(`Total cost: $${totalCost.toFixed(4)}`);
|
|
4059
|
-
console.log();
|
|
4060
|
-
info("Next steps:");
|
|
4061
|
-
info(" 1. Create Remotion project (see remotion-best-practices skill)");
|
|
4062
|
-
info(" 2. Use manifest data to configure video composition");
|
|
4063
|
-
info(" 3. Run: npm start (preview) / npm run render (build)");
|
|
4064
5579
|
} catch (err) {
|
|
4065
5580
|
spinner?.stop();
|
|
4066
5581
|
error(err instanceof Error ? err.message : "Unknown error");
|
|
@@ -4111,10 +5626,135 @@ var searchCommand2 = new Command19("search").description("Search for stock video
|
|
|
4111
5626
|
process.exit(EXIT_CODES.GENERAL_ERROR);
|
|
4112
5627
|
}
|
|
4113
5628
|
});
|
|
4114
|
-
var
|
|
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) => {
|
|
5630
|
+
const format = options.format;
|
|
5631
|
+
const spinner = format === "human" ? ora12("Initializing video project...").start() : null;
|
|
5632
|
+
try {
|
|
5633
|
+
const targetDir = resolve5(process.cwd(), name);
|
|
5634
|
+
try {
|
|
5635
|
+
await access(targetDir);
|
|
5636
|
+
spinner?.stop();
|
|
5637
|
+
error(`Directory "${name}" already exists`);
|
|
5638
|
+
process.exit(EXIT_CODES.INVALID_INPUT);
|
|
5639
|
+
} catch {
|
|
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
|
+
}
|
|
5647
|
+
if (spinner) spinner.text = `Downloading template from ${options.template}...`;
|
|
5648
|
+
try {
|
|
5649
|
+
execSync(`npx --yes degit ${options.template} "${targetDir}"`, {
|
|
5650
|
+
stdio: "pipe"
|
|
5651
|
+
});
|
|
5652
|
+
} catch {
|
|
5653
|
+
if (spinner) spinner.text = "Cloning template...";
|
|
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}"`, {
|
|
5657
|
+
stdio: "pipe"
|
|
5658
|
+
});
|
|
5659
|
+
await rm(join2(targetDir, ".git"), { recursive: true, force: true });
|
|
5660
|
+
}
|
|
5661
|
+
if (format === "human") {
|
|
5662
|
+
spinner?.stop();
|
|
5663
|
+
success(`Template downloaded to ${name}/`);
|
|
5664
|
+
spinner?.start();
|
|
5665
|
+
}
|
|
5666
|
+
if (options.install) {
|
|
5667
|
+
if (spinner) spinner.text = "Installing dependencies...";
|
|
5668
|
+
await new Promise((resolvePromise, reject) => {
|
|
5669
|
+
const child = spawn("pnpm", ["install"], {
|
|
5670
|
+
cwd: targetDir,
|
|
5671
|
+
stdio: "pipe",
|
|
5672
|
+
shell: true
|
|
5673
|
+
});
|
|
5674
|
+
child.on("close", (code) => {
|
|
5675
|
+
if (code === 0) {
|
|
5676
|
+
resolvePromise();
|
|
5677
|
+
} else {
|
|
5678
|
+
reject(new Error(`pnpm install failed with code ${code}`));
|
|
5679
|
+
}
|
|
5680
|
+
});
|
|
5681
|
+
child.on("error", reject);
|
|
5682
|
+
});
|
|
5683
|
+
if (format === "human") {
|
|
5684
|
+
spinner?.stop();
|
|
5685
|
+
success("Dependencies installed");
|
|
5686
|
+
spinner?.start();
|
|
5687
|
+
}
|
|
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
|
+
}
|
|
5713
|
+
spinner?.stop();
|
|
5714
|
+
if (format === "json") {
|
|
5715
|
+
printJson({
|
|
5716
|
+
name,
|
|
5717
|
+
path: targetDir,
|
|
5718
|
+
template: options.template,
|
|
5719
|
+
type: options.type,
|
|
5720
|
+
installed: options.install
|
|
5721
|
+
});
|
|
5722
|
+
return;
|
|
5723
|
+
}
|
|
5724
|
+
if (format === "quiet") {
|
|
5725
|
+
console.log(targetDir);
|
|
5726
|
+
return;
|
|
5727
|
+
}
|
|
5728
|
+
console.log();
|
|
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
|
+
}
|
|
5735
|
+
console.log();
|
|
5736
|
+
info("Next steps:");
|
|
5737
|
+
info(` cd ${name}`);
|
|
5738
|
+
if (!options.install) {
|
|
5739
|
+
info(" pnpm install");
|
|
5740
|
+
}
|
|
5741
|
+
info(" pnpm dev # Preview in Remotion Studio");
|
|
5742
|
+
info(" cc video create ... # Generate assets to public/");
|
|
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
|
+
}
|
|
5748
|
+
} catch (err) {
|
|
5749
|
+
spinner?.stop();
|
|
5750
|
+
error(err instanceof Error ? err.message : "Unknown error");
|
|
5751
|
+
process.exit(EXIT_CODES.GENERAL_ERROR);
|
|
5752
|
+
}
|
|
5753
|
+
});
|
|
5754
|
+
var videoCommand = new Command19("video").description("Video asset generation commands").addCommand(initCommand).addCommand(createCommand3).addCommand(searchCommand2);
|
|
4115
5755
|
|
|
4116
5756
|
// src/index.ts
|
|
4117
|
-
var VERSION = "0.1.
|
|
5757
|
+
var VERSION = "0.1.7";
|
|
4118
5758
|
var program = new Command20();
|
|
4119
5759
|
var cmdName = brand.commands[0];
|
|
4120
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({
|