@conceptcraft/mindframes 0.1.7 → 0.1.8
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/bin/postinstall.js +52 -0
- package/dist/index.js +1162 -1230
- package/dist/index.js.map +1 -1
- package/package.json +14 -3
package/dist/index.js
CHANGED
|
@@ -10,6 +10,64 @@ var __export = (target, all) => {
|
|
|
10
10
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
+
// src/lib/brand.ts
|
|
14
|
+
import path from "path";
|
|
15
|
+
function detectBrand() {
|
|
16
|
+
const binaryPath = process.argv[1] || "";
|
|
17
|
+
const binaryName = path.basename(binaryPath).replace(/\.(js|ts)$/, "");
|
|
18
|
+
const brandId = COMMAND_TO_BRAND[binaryName];
|
|
19
|
+
if (brandId) {
|
|
20
|
+
return BRANDS[brandId];
|
|
21
|
+
}
|
|
22
|
+
for (const [cmd2, id] of Object.entries(COMMAND_TO_BRAND)) {
|
|
23
|
+
if (binaryPath.includes(`/${cmd2}`) || binaryPath.includes(`\\${cmd2}`)) {
|
|
24
|
+
return BRANDS[id];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return BRANDS.mindframes;
|
|
28
|
+
}
|
|
29
|
+
var BRANDS, COMMAND_TO_BRAND, brand;
|
|
30
|
+
var init_brand = __esm({
|
|
31
|
+
"src/lib/brand.ts"() {
|
|
32
|
+
"use strict";
|
|
33
|
+
BRANDS = {
|
|
34
|
+
conceptcraft: {
|
|
35
|
+
id: "conceptcraft",
|
|
36
|
+
name: "conceptcraft",
|
|
37
|
+
displayName: "ConceptCraft",
|
|
38
|
+
description: "CLI tool for ConceptCraft presentation generation",
|
|
39
|
+
commands: ["cc", "conceptcraft"],
|
|
40
|
+
apiUrl: "https://conceptcraft.ai",
|
|
41
|
+
docsUrl: "https://docs.conceptcraft.ai",
|
|
42
|
+
packageName: "@conceptcraft/cli",
|
|
43
|
+
configDir: ".conceptcraft",
|
|
44
|
+
apiKeyEnvVar: "CONCEPTCRAFT_API_KEY",
|
|
45
|
+
apiUrlEnvVar: "CONCEPTCRAFT_API_URL"
|
|
46
|
+
},
|
|
47
|
+
mindframes: {
|
|
48
|
+
id: "mindframes",
|
|
49
|
+
name: "mindframes",
|
|
50
|
+
displayName: "Mindframes",
|
|
51
|
+
description: "CLI tool for Mindframes presentation generation",
|
|
52
|
+
commands: ["mf", "mindframes"],
|
|
53
|
+
apiUrl: "https://mindframes.app",
|
|
54
|
+
docsUrl: "https://docs.mindframes.app",
|
|
55
|
+
packageName: "@conceptcraft/mindframes",
|
|
56
|
+
configDir: ".mindframes",
|
|
57
|
+
apiKeyEnvVar: "MINDFRAMES_API_KEY",
|
|
58
|
+
apiUrlEnvVar: "MINDFRAMES_API_URL"
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
COMMAND_TO_BRAND = {
|
|
62
|
+
cc: "conceptcraft",
|
|
63
|
+
conceptcraft: "conceptcraft",
|
|
64
|
+
mf: "mindframes",
|
|
65
|
+
mindframes: "mindframes"
|
|
66
|
+
};
|
|
67
|
+
brand = detectBrand();
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
13
71
|
// src/lib/config.ts
|
|
14
72
|
import Conf from "conf";
|
|
15
73
|
function getConfig() {
|
|
@@ -25,7 +83,7 @@ function getConfig() {
|
|
|
25
83
|
};
|
|
26
84
|
}
|
|
27
85
|
function getApiKey() {
|
|
28
|
-
const envKey = process.env.
|
|
86
|
+
const envKey = process.env[brand.apiKeyEnvVar] ?? process.env.CC_SLIDES_API_KEY;
|
|
29
87
|
if (envKey) {
|
|
30
88
|
return envKey;
|
|
31
89
|
}
|
|
@@ -35,7 +93,7 @@ function setApiKey(key) {
|
|
|
35
93
|
config.set("apiKey", key);
|
|
36
94
|
}
|
|
37
95
|
function getApiUrl() {
|
|
38
|
-
const envUrl = process.env.
|
|
96
|
+
const envUrl = process.env[brand.apiUrlEnvVar] ?? process.env.CC_SLIDES_API_URL;
|
|
39
97
|
if (envUrl) {
|
|
40
98
|
return envUrl;
|
|
41
99
|
}
|
|
@@ -97,6 +155,7 @@ var DEFAULT_API_URL, schema, config;
|
|
|
97
155
|
var init_config = __esm({
|
|
98
156
|
"src/lib/config.ts"() {
|
|
99
157
|
"use strict";
|
|
158
|
+
init_brand();
|
|
100
159
|
DEFAULT_API_URL = "https://www.mindframes.app";
|
|
101
160
|
schema = {
|
|
102
161
|
apiKey: {
|
|
@@ -128,7 +187,7 @@ var init_config = __esm({
|
|
|
128
187
|
}
|
|
129
188
|
};
|
|
130
189
|
config = new Conf({
|
|
131
|
-
projectName:
|
|
190
|
+
projectName: brand.name,
|
|
132
191
|
schema
|
|
133
192
|
});
|
|
134
193
|
}
|
|
@@ -440,7 +499,7 @@ async function request(endpoint, options = {}) {
|
|
|
440
499
|
const apiUrl = getApiUrl();
|
|
441
500
|
if (!hasAuth()) {
|
|
442
501
|
throw new ApiError(
|
|
443
|
-
|
|
502
|
+
`Not authenticated. Run '${brand.commands[0]} login' or set ${brand.apiKeyEnvVar} environment variable.`,
|
|
444
503
|
401,
|
|
445
504
|
2
|
|
446
505
|
// AUTH_ERROR
|
|
@@ -509,7 +568,7 @@ async function streamRequest(endpoint, body) {
|
|
|
509
568
|
const apiUrl = getApiUrl();
|
|
510
569
|
if (!hasAuth()) {
|
|
511
570
|
throw new ApiError(
|
|
512
|
-
|
|
571
|
+
`Not authenticated. Run '${brand.commands[0]} login' or set ${brand.apiKeyEnvVar} environment variable.`,
|
|
513
572
|
401,
|
|
514
573
|
2
|
|
515
574
|
);
|
|
@@ -585,7 +644,7 @@ async function uploadFile(filePath) {
|
|
|
585
644
|
const apiUrl = getApiUrl();
|
|
586
645
|
if (!hasAuth()) {
|
|
587
646
|
throw new ApiError(
|
|
588
|
-
|
|
647
|
+
`Not authenticated. Run '${brand.commands[0]} login' or set ${brand.apiKeyEnvVar} environment variable.`,
|
|
589
648
|
401,
|
|
590
649
|
2
|
|
591
650
|
);
|
|
@@ -745,7 +804,7 @@ async function exportPresentation(presentationId, options = {}) {
|
|
|
745
804
|
const apiUrl = getApiUrl();
|
|
746
805
|
if (!hasAuth()) {
|
|
747
806
|
throw new ApiError(
|
|
748
|
-
|
|
807
|
+
`Not authenticated. Run '${brand.commands[0]} login' or set ${brand.apiKeyEnvVar} environment variable.`,
|
|
749
808
|
401,
|
|
750
809
|
2
|
|
751
810
|
);
|
|
@@ -779,7 +838,7 @@ async function importPresentation(fileBuffer, fileName, options = {}) {
|
|
|
779
838
|
const apiUrl = getApiUrl();
|
|
780
839
|
if (!hasAuth()) {
|
|
781
840
|
throw new ApiError(
|
|
782
|
-
|
|
841
|
+
`Not authenticated. Run '${brand.commands[0]} login' or set ${brand.apiKeyEnvVar} environment variable.`,
|
|
783
842
|
401,
|
|
784
843
|
2
|
|
785
844
|
);
|
|
@@ -832,7 +891,7 @@ async function generateBlog(presentationId, documentCreatedAt, options = {}) {
|
|
|
832
891
|
const apiUrl = getApiUrl();
|
|
833
892
|
if (!hasAuth()) {
|
|
834
893
|
throw new ApiError(
|
|
835
|
-
|
|
894
|
+
`Not authenticated. Run '${brand.commands[0]} login' or set ${brand.apiKeyEnvVar} environment variable.`,
|
|
836
895
|
401,
|
|
837
896
|
2
|
|
838
897
|
);
|
|
@@ -907,7 +966,7 @@ async function generateSpeech(ttsRequest) {
|
|
|
907
966
|
const apiUrl = getApiUrl();
|
|
908
967
|
if (!hasAuth()) {
|
|
909
968
|
throw new ApiError(
|
|
910
|
-
|
|
969
|
+
`Not authenticated. Run '${brand.commands[0]} login' or set ${brand.apiKeyEnvVar} environment variable.`,
|
|
911
970
|
401,
|
|
912
971
|
2
|
|
913
972
|
);
|
|
@@ -1035,6 +1094,7 @@ var init_api = __esm({
|
|
|
1035
1094
|
"use strict";
|
|
1036
1095
|
init_config();
|
|
1037
1096
|
init_auth();
|
|
1097
|
+
init_brand();
|
|
1038
1098
|
ApiError = class extends Error {
|
|
1039
1099
|
constructor(message, statusCode, exitCode = 1) {
|
|
1040
1100
|
super(message);
|
|
@@ -1395,60 +1455,11 @@ var init_login = __esm({
|
|
|
1395
1455
|
});
|
|
1396
1456
|
|
|
1397
1457
|
// src/index.ts
|
|
1458
|
+
init_brand();
|
|
1459
|
+
init_login();
|
|
1398
1460
|
import { Command as Command20 } from "commander";
|
|
1399
1461
|
import chalk13 from "chalk";
|
|
1400
1462
|
|
|
1401
|
-
// src/lib/brand.ts
|
|
1402
|
-
import path from "path";
|
|
1403
|
-
var BRANDS = {
|
|
1404
|
-
conceptcraft: {
|
|
1405
|
-
id: "conceptcraft",
|
|
1406
|
-
name: "conceptcraft",
|
|
1407
|
-
displayName: "ConceptCraft",
|
|
1408
|
-
description: "CLI tool for ConceptCraft presentation generation",
|
|
1409
|
-
commands: ["cc", "conceptcraft"],
|
|
1410
|
-
apiUrl: "https://conceptcraft.ai",
|
|
1411
|
-
docsUrl: "https://docs.conceptcraft.ai",
|
|
1412
|
-
packageName: "@conceptcraft/cli",
|
|
1413
|
-
configDir: ".conceptcraft"
|
|
1414
|
-
},
|
|
1415
|
-
mindframes: {
|
|
1416
|
-
id: "mindframes",
|
|
1417
|
-
name: "mindframes",
|
|
1418
|
-
displayName: "Mindframes",
|
|
1419
|
-
description: "CLI tool for Mindframes presentation generation",
|
|
1420
|
-
commands: ["mf", "mindframes"],
|
|
1421
|
-
apiUrl: "https://mindframes.app",
|
|
1422
|
-
docsUrl: "https://docs.mindframes.app",
|
|
1423
|
-
packageName: "@mindframes/cli",
|
|
1424
|
-
configDir: ".mindframes"
|
|
1425
|
-
}
|
|
1426
|
-
};
|
|
1427
|
-
var COMMAND_TO_BRAND = {
|
|
1428
|
-
cc: "conceptcraft",
|
|
1429
|
-
conceptcraft: "conceptcraft",
|
|
1430
|
-
mf: "mindframes",
|
|
1431
|
-
mindframes: "mindframes"
|
|
1432
|
-
};
|
|
1433
|
-
function detectBrand() {
|
|
1434
|
-
const binaryPath = process.argv[1] || "";
|
|
1435
|
-
const binaryName = path.basename(binaryPath).replace(/\.(js|ts)$/, "");
|
|
1436
|
-
const brandId = COMMAND_TO_BRAND[binaryName];
|
|
1437
|
-
if (brandId) {
|
|
1438
|
-
return BRANDS[brandId];
|
|
1439
|
-
}
|
|
1440
|
-
for (const [cmd2, id] of Object.entries(COMMAND_TO_BRAND)) {
|
|
1441
|
-
if (binaryPath.includes(`/${cmd2}`) || binaryPath.includes(`\\${cmd2}`)) {
|
|
1442
|
-
return BRANDS[id];
|
|
1443
|
-
}
|
|
1444
|
-
}
|
|
1445
|
-
return BRANDS.mindframes;
|
|
1446
|
-
}
|
|
1447
|
-
var brand = detectBrand();
|
|
1448
|
-
|
|
1449
|
-
// src/index.ts
|
|
1450
|
-
init_login();
|
|
1451
|
-
|
|
1452
1463
|
// src/commands/logout.ts
|
|
1453
1464
|
init_config();
|
|
1454
1465
|
init_feature_cache();
|
|
@@ -3129,569 +3140,422 @@ var whoamiCommand = new Command13("whoami").description("Show current user and t
|
|
|
3129
3140
|
|
|
3130
3141
|
// src/commands/skill/index.ts
|
|
3131
3142
|
init_output();
|
|
3143
|
+
init_brand();
|
|
3132
3144
|
import { Command as Command14 } from "commander";
|
|
3133
3145
|
import chalk12 from "chalk";
|
|
3134
3146
|
|
|
3135
|
-
// src/commands/skill/
|
|
3136
|
-
var
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
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
|
|
3148
|
-
|
|
3149
|
-
Create professional presentations directly from your terminal. The CLI generates AI-powered slides from any context you provide - text, files, URLs, or piped content.`
|
|
3150
|
-
};
|
|
3151
|
-
|
|
3152
|
-
// src/commands/skill/sections/prerequisites.ts
|
|
3153
|
-
var prerequisites = {
|
|
3154
|
-
title: "Prerequisites",
|
|
3155
|
-
render: (ctx) => `## Prerequisites
|
|
3156
|
-
|
|
3157
|
-
\`\`\`bash
|
|
3158
|
-
npm install -g ${ctx.pkg}
|
|
3159
|
-
${ctx.cmd} login # Authenticate (opens browser)
|
|
3160
|
-
${ctx.cmd} whoami # Verify auth
|
|
3161
|
-
\`\`\`
|
|
3147
|
+
// src/commands/skill/rules/video/content.ts
|
|
3148
|
+
var VIDEO_RULE_CONTENTS = [
|
|
3149
|
+
{
|
|
3150
|
+
filename: "video-creation-guide.md",
|
|
3151
|
+
content: `# Video Creation Guide
|
|
3162
3152
|
|
|
3163
|
-
###
|
|
3153
|
+
### Related Skills
|
|
3164
3154
|
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3155
|
+
Use these installed skills for implementation details:
|
|
3156
|
+
- \`remotion-best-practices\` \u2014 Remotion patterns and API
|
|
3157
|
+
- \`threejs-*\` skills \u2014 for R3F/WebGL (particles, 3D elements)
|
|
3168
3158
|
|
|
3169
|
-
|
|
3170
|
-
var workflow = {
|
|
3171
|
-
title: "Core Workflow",
|
|
3172
|
-
render: (ctx) => `## Core Workflow
|
|
3159
|
+
---
|
|
3173
3160
|
|
|
3174
|
-
|
|
3175
|
-
2. **Create presentation** - Pass context to \`${ctx.cmd} create\`
|
|
3176
|
-
3. **Share URL** - Return the presentation link to the user`
|
|
3177
|
-
};
|
|
3161
|
+
## Core Rules
|
|
3178
3162
|
|
|
3179
|
-
|
|
3180
|
-
var createCommand2 = {
|
|
3181
|
-
title: "Create Command",
|
|
3182
|
-
render: (ctx) => `## Commands
|
|
3163
|
+
Your task is not "making slideshows" \u2014 you are **simulating a real interface** that obeys cinematic physics.
|
|
3183
3164
|
|
|
3184
|
-
###
|
|
3165
|
+
### Hard Constraints
|
|
3185
3166
|
|
|
3186
|
-
|
|
3167
|
+
1. **No scene > 8 seconds** without cut or major action
|
|
3168
|
+
2. **No static pixels** \u2014 everything breathes, drifts, pulses
|
|
3169
|
+
3. **No linear interpolation** \u2014 use \`spring()\` physics
|
|
3170
|
+
4. **Scene overlap 15-20 frames** \u2014 no hard cuts
|
|
3171
|
+
5. **60 FPS mandatory** \u2014 30fps looks choppy
|
|
3172
|
+
6. **No screenshots for UI** \u2014 rebuild in React/CSS
|
|
3187
3173
|
|
|
3188
|
-
|
|
3189
|
-
# Upload files (PDFs, PPTX, images, docs)
|
|
3190
|
-
${ctx.cmd} create "Product Overview" --file ./deck.pptx --file ./logo.png
|
|
3174
|
+
---
|
|
3191
3175
|
|
|
3192
|
-
|
|
3193
|
-
${ctx.cmd} create "Topic Title" --context "Key points, data, facts..."
|
|
3176
|
+
## Code Organization
|
|
3194
3177
|
|
|
3195
|
-
|
|
3196
|
-
|
|
3178
|
+
- Create separate files: \`Button.tsx\`, \`Window.tsx\`, \`Cursor.tsx\`
|
|
3179
|
+
- Use Zod schemas for props validation
|
|
3180
|
+
- Extract animation configs to constants
|
|
3197
3181
|
|
|
3198
|
-
|
|
3199
|
-
|
|
3182
|
+
\`\`\`tsx
|
|
3183
|
+
import { spring, interpolate, useCurrentFrame, useVideoConfig } from 'remotion';
|
|
3200
3184
|
|
|
3201
|
-
|
|
3202
|
-
|
|
3185
|
+
// ALWAYS use spring for element entrances
|
|
3186
|
+
// NEVER use magic numbers
|
|
3187
|
+
\`\`\`
|
|
3203
3188
|
|
|
3204
|
-
|
|
3205
|
-
cat src/auth/*.ts | ${ctx.cmd} create "Auth System" \\
|
|
3206
|
-
--file ./architecture.png \\
|
|
3207
|
-
--context "Focus on security patterns"
|
|
3208
|
-
\`\`\``
|
|
3209
|
-
};
|
|
3189
|
+
---
|
|
3210
3190
|
|
|
3211
|
-
|
|
3212
|
-
var createOptions = {
|
|
3213
|
-
title: "Create Options",
|
|
3214
|
-
render: (_ctx) => `### Create Options
|
|
3215
|
-
|
|
3216
|
-
| Option | Description | Default |
|
|
3217
|
-
|--------|-------------|---------|
|
|
3218
|
-
| \`-n, --slides <count>\` | Number of slides (1-20) | 10 |
|
|
3219
|
-
| \`-m, --mode <mode>\` | Quality: \`instant\`, \`fast\`, \`balanced\`, \`best\` | balanced |
|
|
3220
|
-
| \`-t, --tone <tone>\` | Tone: \`professional\`, \`educational\`, \`creative\`, \`formal\`, \`casual\` | professional |
|
|
3221
|
-
| \`--amount <amount>\` | Density: \`minimal\`, \`concise\`, \`detailed\`, \`extensive\` | concise |
|
|
3222
|
-
| \`--audience <text>\` | Target audience | General Audience |
|
|
3223
|
-
| \`-g, --goal <type>\` | Purpose: \`inform\`, \`persuade\`, \`train\`, \`learn\`, \`entertain\`, \`report\` | - |
|
|
3224
|
-
| \`--custom-goal <text>\` | Custom goal description | - |
|
|
3225
|
-
| \`-f, --file <paths...>\` | Files to upload (PDF, PPTX, images, docs) | - |
|
|
3226
|
-
| \`-l, --language <lang>\` | Output language | en |
|
|
3227
|
-
| \`-b, --brand <id>\` | Branding ID to apply | - |
|
|
3228
|
-
| \`-o, --output <format>\` | Output: \`human\`, \`json\`, \`quiet\` | human |`
|
|
3229
|
-
};
|
|
3191
|
+
## Aesthetics (Linear/Stripe Style)
|
|
3230
3192
|
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
render: (ctx) => `### Other Commands
|
|
3193
|
+
\`\`\`css
|
|
3194
|
+
/* Shadows - soft, expensive */
|
|
3195
|
+
box-shadow: 0 20px 50px -12px rgba(0,0,0,0.5);
|
|
3235
3196
|
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3197
|
+
/* Borders - thin, barely visible */
|
|
3198
|
+
border: 1px solid rgba(255,255,255,0.1);
|
|
3199
|
+
\`\`\`
|
|
3239
3200
|
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3201
|
+
- Fonts: Inter or SF Pro
|
|
3202
|
+
- Never pure \`#000000\` \u2014 use \`#050505\`
|
|
3203
|
+
- Never pure \`#FFFFFF\` \u2014 use \`#F0F0F0\`
|
|
3243
3204
|
|
|
3244
|
-
|
|
3245
|
-
${ctx.cmd} get <id-or-slug>
|
|
3205
|
+
---
|
|
3246
3206
|
|
|
3247
|
-
|
|
3248
|
-
${ctx.cmd} export <id-or-slug> -o presentation.zip
|
|
3207
|
+
## Self-Check Before Render
|
|
3249
3208
|
|
|
3250
|
-
|
|
3251
|
-
|
|
3209
|
+
- [ ] Camera rig wraps entire scene with drift/zoom
|
|
3210
|
+
- [ ] Every UI element uses 2.5D rotation entrance
|
|
3211
|
+
- [ ] Cursor moves in curves with overshoot
|
|
3212
|
+
- [ ] Lists/grids stagger (never appear all at once)
|
|
3213
|
+
- [ ] Background has moving orbs + vignette + noise
|
|
3214
|
+
- [ ] Something is moving on EVERY frame
|
|
3215
|
+
- [ ] Scene transitions overlap (no hard cuts)
|
|
3252
3216
|
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3217
|
+
**If your video looks like PowerPoint with voiceover \u2014 START OVER.**
|
|
3218
|
+
`
|
|
3219
|
+
},
|
|
3220
|
+
{
|
|
3221
|
+
filename: "animation-physics.md",
|
|
3222
|
+
content: `# Animation Physics
|
|
3256
3223
|
|
|
3257
|
-
|
|
3258
|
-
${ctx.cmd} skill install
|
|
3259
|
-
${ctx.cmd} skill show
|
|
3260
|
-
\`\`\``
|
|
3261
|
-
};
|
|
3224
|
+
## Spring Configurations
|
|
3262
3225
|
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3226
|
+
### Heavy UI (Modals, Sidebars)
|
|
3227
|
+
\`\`\`tsx
|
|
3228
|
+
config: { mass: 1, stiffness: 100, damping: 15 }
|
|
3229
|
+
\`\`\`
|
|
3267
3230
|
|
|
3268
|
-
###
|
|
3231
|
+
### Light UI (Tooltips, Badges)
|
|
3232
|
+
\`\`\`tsx
|
|
3233
|
+
config: { mass: 0.6, stiffness: 180, damping: 12 }
|
|
3234
|
+
\`\`\`
|
|
3269
3235
|
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
--slides 8 --tone educational --audience "New developers" \\
|
|
3274
|
-
--goal train
|
|
3236
|
+
### Standard (Snappy)
|
|
3237
|
+
\`\`\`tsx
|
|
3238
|
+
config: { mass: 1, damping: 15, stiffness: 120 }
|
|
3275
3239
|
\`\`\`
|
|
3276
3240
|
|
|
3277
|
-
|
|
3241
|
+
---
|
|
3278
3242
|
|
|
3279
|
-
|
|
3280
|
-
${ctx.cmd} create "API Reference" \\
|
|
3281
|
-
--file ./docs/api.md \\
|
|
3282
|
-
--file ./diagrams/architecture.png \\
|
|
3283
|
-
--mode best --amount detailed \\
|
|
3284
|
-
--goal inform
|
|
3285
|
-
\`\`\`
|
|
3243
|
+
## Staggering
|
|
3286
3244
|
|
|
3287
|
-
|
|
3245
|
+
**NEVER show a list all at once.**
|
|
3288
3246
|
|
|
3289
|
-
\`\`\`
|
|
3290
|
-
|
|
3291
|
-
-m instant --slides 5
|
|
3292
|
-
\`\`\`
|
|
3247
|
+
\`\`\`tsx
|
|
3248
|
+
const STAGGER_FRAMES = 3; // 3-5 frames between items
|
|
3293
3249
|
|
|
3294
|
-
|
|
3250
|
+
{items.map((item, i) => {
|
|
3251
|
+
const delay = i * STAGGER_FRAMES;
|
|
3252
|
+
const progress = spring({
|
|
3253
|
+
frame: frame - delay,
|
|
3254
|
+
fps,
|
|
3255
|
+
config: { damping: 15, stiffness: 120 },
|
|
3256
|
+
});
|
|
3295
3257
|
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3258
|
+
return (
|
|
3259
|
+
<div style={{
|
|
3260
|
+
opacity: progress,
|
|
3261
|
+
transform: \`translateY(\${interpolate(progress, [0, 1], [20, 0])}px)\`,
|
|
3262
|
+
}}>
|
|
3263
|
+
{item}
|
|
3264
|
+
</div>
|
|
3265
|
+
);
|
|
3266
|
+
})}
|
|
3302
3267
|
\`\`\`
|
|
3303
3268
|
|
|
3304
|
-
|
|
3269
|
+
---
|
|
3305
3270
|
|
|
3306
|
-
|
|
3307
|
-
${ctx.cmd} create "Market Analysis" \\
|
|
3308
|
-
--file ./research.pdf \\
|
|
3309
|
-
--sources https://report.com/industry.pdf \\
|
|
3310
|
-
--tone formal --audience "Executive team" \\
|
|
3311
|
-
--goal report
|
|
3312
|
-
\`\`\``
|
|
3313
|
-
};
|
|
3271
|
+
## Cursor Movement
|
|
3314
3272
|
|
|
3315
|
-
|
|
3316
|
-
var output = {
|
|
3317
|
-
title: "Output",
|
|
3318
|
-
render: (ctx) => `## Output
|
|
3273
|
+
**Cursor NEVER moves in straight lines.**
|
|
3319
3274
|
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3275
|
+
\`\`\`tsx
|
|
3276
|
+
const progress = spring({
|
|
3277
|
+
frame: frame - startFrame,
|
|
3278
|
+
fps,
|
|
3279
|
+
config: { damping: 20, stiffness: 80 },
|
|
3280
|
+
});
|
|
3323
3281
|
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
Generated in: 45s \xB7 12,500 tokens
|
|
3282
|
+
const linearX = interpolate(progress, [0, 1], [start.x, end.x]);
|
|
3283
|
+
const linearY = interpolate(progress, [0, 1], [start.y, end.y]);
|
|
3327
3284
|
|
|
3328
|
-
|
|
3285
|
+
// THE ARC: Parabola that peaks mid-travel
|
|
3286
|
+
const arcHeight = 100;
|
|
3287
|
+
const arcOffset = Math.sin(progress * Math.PI) * arcHeight;
|
|
3288
|
+
const cursorY = linearY - arcOffset;
|
|
3329
3289
|
\`\`\`
|
|
3330
3290
|
|
|
3331
|
-
|
|
3332
|
-
\`\`\`bash
|
|
3333
|
-
URL=$(${ctx.cmd} create "Demo" --context "..." -o json | jq -r '.viewUrl')
|
|
3334
|
-
\`\`\``
|
|
3335
|
-
};
|
|
3291
|
+
---
|
|
3336
3292
|
|
|
3337
|
-
|
|
3338
|
-
var bestPractices = {
|
|
3339
|
-
title: "Best Practices",
|
|
3340
|
-
render: (_ctx) => `## Best Practices
|
|
3341
|
-
|
|
3342
|
-
1. **Provide rich context** - More context = better slides. Include code, docs, data.
|
|
3343
|
-
2. **Use file uploads for binary content** - PDFs, images, PPTX files need \`--file\`.
|
|
3344
|
-
3. **Specify a goal** - Helps tailor the presentation structure and messaging.
|
|
3345
|
-
4. **Use appropriate mode** - \`instant\` for quick drafts, \`best\` for important presentations.
|
|
3346
|
-
5. **Specify audience** - Helps tailor complexity and terminology.
|
|
3347
|
-
6. **Combine sources** - Pipe multiple files for comprehensive presentations.`
|
|
3348
|
-
};
|
|
3293
|
+
## Click Interaction
|
|
3349
3294
|
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3295
|
+
\`\`\`tsx
|
|
3296
|
+
// On click:
|
|
3297
|
+
const cursorScale = isClicking ? 0.95 : 1;
|
|
3298
|
+
const buttonScaleX = isClicking ? 1.02 : 1;
|
|
3299
|
+
const buttonScaleY = isClicking ? 0.95 : 1;
|
|
3300
|
+
// Release both with spring
|
|
3301
|
+
\`\`\`
|
|
3354
3302
|
|
|
3355
|
-
|
|
3356
|
-
- **Images**: JPEG, PNG, GIF, WebP
|
|
3357
|
-
- **Text**: Markdown, TXT, CSV, JSON`
|
|
3358
|
-
};
|
|
3303
|
+
---
|
|
3359
3304
|
|
|
3360
|
-
|
|
3361
|
-
var troubleshooting = {
|
|
3362
|
-
title: "Troubleshooting",
|
|
3363
|
-
render: (ctx) => `## Troubleshooting
|
|
3305
|
+
## Timing Reference
|
|
3364
3306
|
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3307
|
+
| Action | Frames (60fps) |
|
|
3308
|
+
|--------|----------------|
|
|
3309
|
+
| Element entrance | 15-20 |
|
|
3310
|
+
| Stagger gap | 3-5 |
|
|
3311
|
+
| Hold on key info | 45-60 |
|
|
3312
|
+
| Scene transition | 20-30 |
|
|
3313
|
+
| Fast interaction | 15-20 |
|
|
3314
|
+
`
|
|
3315
|
+
},
|
|
3316
|
+
{
|
|
3317
|
+
filename: "scene-structure.md",
|
|
3318
|
+
content: `# Scene Structure
|
|
3368
3319
|
|
|
3369
|
-
|
|
3370
|
-
${ctx.cmd} login
|
|
3320
|
+
## SceneWrapper
|
|
3371
3321
|
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3322
|
+
\`\`\`tsx
|
|
3323
|
+
<SceneWrapper
|
|
3324
|
+
durationInFrames={300}
|
|
3325
|
+
transitionType="slideLeft"
|
|
3326
|
+
cameraMotion="panRight"
|
|
3327
|
+
>
|
|
3328
|
+
<FeatureLayer />
|
|
3329
|
+
<CursorLayer />
|
|
3330
|
+
<ParticleLayer />
|
|
3331
|
+
</SceneWrapper>
|
|
3332
|
+
\`\`\`
|
|
3376
3333
|
|
|
3377
|
-
|
|
3378
|
-
var videoCreation = {
|
|
3379
|
-
title: "Video Creation",
|
|
3380
|
-
render: (ctx) => `## Video Creation
|
|
3334
|
+
---
|
|
3381
3335
|
|
|
3382
|
-
|
|
3336
|
+
## Layer Structure (Z-Index)
|
|
3383
3337
|
|
|
3384
|
-
|
|
3338
|
+
| Layer | Z-Index |
|
|
3339
|
+
|-------|---------|
|
|
3340
|
+
| Background orbs | 0 |
|
|
3341
|
+
| Vignette | 1 |
|
|
3342
|
+
| UI Base | 10 |
|
|
3343
|
+
| UI Elements | 20 |
|
|
3344
|
+
| Overlays | 30 |
|
|
3345
|
+
| Text/Captions | 40 |
|
|
3346
|
+
| Cursor | 50 |
|
|
3385
3347
|
|
|
3386
|
-
|
|
3348
|
+
---
|
|
3387
3349
|
|
|
3388
|
-
|
|
3350
|
+
## Case Study: SaaS Task Tracker
|
|
3389
3351
|
|
|
3390
|
-
|
|
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 |
|
|
3352
|
+
### Scene 1: "The Hook" (~5s)
|
|
3398
3353
|
|
|
3399
|
-
|
|
3354
|
+
1. Dark background (\`#0B0C10\`), grid drifting
|
|
3355
|
+
2. Scattered circles magnetically attract \u2192 morph into logo
|
|
3356
|
+
3. Logo expands \u2192 becomes sidebar navigation
|
|
3400
3357
|
|
|
3401
|
-
###
|
|
3358
|
+
### Scene 2: "Micro-Interaction" (~6s)
|
|
3402
3359
|
|
|
3403
|
-
|
|
3360
|
+
1. Modal "Create Issue" appears
|
|
3361
|
+
2. Text types character by character (non-uniform speed)
|
|
3362
|
+
3. \`CMD + K\` hint glows, keys animate
|
|
3363
|
+
4. Cursor flies to "Save" in arc, slows on approach
|
|
3404
3364
|
|
|
3405
|
-
|
|
3365
|
+
### Scene 3: "The Connection" (~5s)
|
|
3406
3366
|
|
|
3407
|
-
|
|
3367
|
+
1. Task card grabbed, scales 1.05, shadow deepens
|
|
3368
|
+
2. Other cards spread apart
|
|
3369
|
+
3. **Match Cut:** Zoom into avatar \u2192 color fills screen \u2192 becomes mobile notification background
|
|
3408
3370
|
|
|
3409
|
-
|
|
3371
|
+
---
|
|
3410
3372
|
|
|
3411
|
-
|
|
3373
|
+
## Composition
|
|
3412
3374
|
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3375
|
+
\`\`\`tsx
|
|
3376
|
+
<AbsoluteFill>
|
|
3377
|
+
<MovingBackground />
|
|
3378
|
+
<Vignette />
|
|
3379
|
+
<CameraRig>
|
|
3380
|
+
<Sequence from={0} durationInFrames={100}>
|
|
3381
|
+
<Scene1 />
|
|
3382
|
+
</Sequence>
|
|
3383
|
+
<Sequence from={85} durationInFrames={150}> {/* 15 frame overlap! */}
|
|
3384
|
+
<Scene2 />
|
|
3385
|
+
</Sequence>
|
|
3386
|
+
</CameraRig>
|
|
3387
|
+
<Audio src={music} volume={0.3} />
|
|
3388
|
+
</AbsoluteFill>
|
|
3389
|
+
\`\`\`
|
|
3390
|
+
`
|
|
3391
|
+
},
|
|
3392
|
+
{
|
|
3393
|
+
filename: "scene-transitions.md",
|
|
3394
|
+
content: `# Scene Transitions
|
|
3419
3395
|
|
|
3420
|
-
|
|
3396
|
+
**No FadeIn/FadeOut.** Only contextual transitions.
|
|
3421
3397
|
|
|
3422
|
-
|
|
3398
|
+
---
|
|
3423
3399
|
|
|
3424
|
-
|
|
3400
|
+
## Types
|
|
3425
3401
|
|
|
3426
|
-
###
|
|
3402
|
+
### 1. Object Persistence
|
|
3403
|
+
Same chart transforms (data, color, scale) while UI changes around it.
|
|
3427
3404
|
|
|
3428
|
-
|
|
3429
|
-
|
|
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
|
|
3405
|
+
### 2. Mask Reveal
|
|
3406
|
+
Button expands to screen size via SVG \`clipPath\`.
|
|
3436
3407
|
|
|
3437
|
-
###
|
|
3408
|
+
### 3. Speed Ramps
|
|
3409
|
+
Scene A accelerates out, Scene B starts fast then slows.
|
|
3438
3410
|
|
|
3439
|
-
|
|
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
|
-
};
|
|
3411
|
+
---
|
|
3453
3412
|
|
|
3454
|
-
|
|
3455
|
-
var DEFAULT_SECTIONS = [
|
|
3456
|
-
frontmatter,
|
|
3457
|
-
header3,
|
|
3458
|
-
prerequisites,
|
|
3459
|
-
workflow,
|
|
3460
|
-
createCommand2,
|
|
3461
|
-
createOptions,
|
|
3462
|
-
otherCommands,
|
|
3463
|
-
examples,
|
|
3464
|
-
output,
|
|
3465
|
-
videoCreation,
|
|
3466
|
-
bestPractices,
|
|
3467
|
-
fileTypes,
|
|
3468
|
-
troubleshooting
|
|
3469
|
-
];
|
|
3470
|
-
function createSkillContext(brand2) {
|
|
3471
|
-
return {
|
|
3472
|
-
cmd: brand2.name,
|
|
3473
|
-
pkg: brand2.packageName,
|
|
3474
|
-
url: brand2.apiUrl,
|
|
3475
|
-
name: brand2.displayName
|
|
3476
|
-
};
|
|
3477
|
-
}
|
|
3478
|
-
function generateSkillContent(brand2, sections = DEFAULT_SECTIONS) {
|
|
3479
|
-
const ctx = createSkillContext(brand2);
|
|
3480
|
-
return sections.map((section) => section.render(ctx)).join("\n\n");
|
|
3481
|
-
}
|
|
3413
|
+
## Match Cut Example
|
|
3482
3414
|
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3415
|
+
\`\`\`
|
|
3416
|
+
Scene A: Zoom into avatar
|
|
3417
|
+
\u2193
|
|
3418
|
+
Avatar color fills screen
|
|
3419
|
+
\u2193
|
|
3420
|
+
Scene B: That color IS the notification background
|
|
3421
|
+
\`\`\`
|
|
3487
3422
|
|
|
3488
|
-
|
|
3489
|
-
var SUPPORTED_EDITORS = [
|
|
3490
|
-
{ name: "Claude Code", dir: ".claude" },
|
|
3491
|
-
{ name: "Cursor", dir: ".cursor" },
|
|
3492
|
-
{ name: "Codex", dir: ".codex" },
|
|
3493
|
-
{ name: "OpenCode", dir: ".opencode" },
|
|
3494
|
-
{ name: "Windsurf", dir: ".windsurf" },
|
|
3495
|
-
{ name: "Agent", dir: ".agent" }
|
|
3496
|
-
];
|
|
3423
|
+
---
|
|
3497
3424
|
|
|
3498
|
-
|
|
3499
|
-
var VIDEO_RULE_CONTENTS = [
|
|
3500
|
-
{
|
|
3501
|
-
filename: "video-creation-guide.md",
|
|
3502
|
-
content: `# Video Creation Guide
|
|
3425
|
+
## Overlapping Sequences (CRITICAL)
|
|
3503
3426
|
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3427
|
+
\`\`\`tsx
|
|
3428
|
+
<Sequence from={0} durationInFrames={100}>
|
|
3429
|
+
<SceneOne />
|
|
3430
|
+
</Sequence>
|
|
3431
|
+
<Sequence from={85} durationInFrames={150}> {/* 15 frames early! */}
|
|
3432
|
+
<SceneTwo />
|
|
3433
|
+
</Sequence>
|
|
3434
|
+
\`\`\`
|
|
3507
3435
|
|
|
3508
3436
|
---
|
|
3509
3437
|
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
**Objective:** Replicate the high-energy, fluid feel of premium tech product videos (Stripe, Apple, Linear, Affable.ai).
|
|
3438
|
+
## TransitionSeries
|
|
3513
3439
|
|
|
3514
|
-
|
|
3440
|
+
\`\`\`tsx
|
|
3441
|
+
import { TransitionSeries, linearTiming } from '@remotion/transitions';
|
|
3442
|
+
import { slide } from '@remotion/transitions/slide';
|
|
3443
|
+
|
|
3444
|
+
<TransitionSeries>
|
|
3445
|
+
<TransitionSeries.Sequence durationInFrames={100}>
|
|
3446
|
+
<SceneOne />
|
|
3447
|
+
</TransitionSeries.Sequence>
|
|
3448
|
+
<TransitionSeries.Transition
|
|
3449
|
+
presentation={slide({ direction: 'from-bottom' })}
|
|
3450
|
+
timing={linearTiming({ durationInFrames: 20 })}
|
|
3451
|
+
/>
|
|
3452
|
+
<TransitionSeries.Sequence durationInFrames={150}>
|
|
3453
|
+
<SceneTwo />
|
|
3454
|
+
</TransitionSeries.Sequence>
|
|
3455
|
+
</TransitionSeries>
|
|
3456
|
+
\`\`\`
|
|
3457
|
+
`
|
|
3458
|
+
},
|
|
3459
|
+
{
|
|
3460
|
+
filename: "polish-effects.md",
|
|
3461
|
+
content: `# Polish Effects
|
|
3515
3462
|
|
|
3516
|
-
|
|
3463
|
+
## Reflection (Glass Glint)
|
|
3517
3464
|
|
|
3518
|
-
|
|
3465
|
+
Diagonal gradient sweeps every 5 seconds.
|
|
3519
3466
|
|
|
3520
|
-
|
|
3467
|
+
\`\`\`tsx
|
|
3468
|
+
const cycleFrame = frame % 300;
|
|
3469
|
+
const sweepProgress = interpolate(cycleFrame, [0, 60], [-100, 200], {
|
|
3470
|
+
extrapolateRight: 'clamp',
|
|
3471
|
+
});
|
|
3472
|
+
\`\`\`
|
|
3521
3473
|
|
|
3522
|
-
|
|
3474
|
+
---
|
|
3523
3475
|
|
|
3524
|
-
|
|
3476
|
+
## Background Breathing
|
|
3525
3477
|
|
|
3526
|
-
|
|
3478
|
+
Background is NEVER static.
|
|
3527
3479
|
|
|
3528
3480
|
\`\`\`tsx
|
|
3529
|
-
const
|
|
3530
|
-
|
|
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
|
-
};
|
|
3481
|
+
const orb1X = Math.sin(frame / 60) * 200;
|
|
3482
|
+
const orb1Y = Math.cos(frame / 80) * 100;
|
|
3545
3483
|
\`\`\`
|
|
3546
3484
|
|
|
3547
|
-
|
|
3485
|
+
---
|
|
3548
3486
|
|
|
3549
|
-
|
|
3487
|
+
## Typewriter Effect
|
|
3550
3488
|
|
|
3551
3489
|
\`\`\`tsx
|
|
3552
|
-
|
|
3553
|
-
const
|
|
3554
|
-
|
|
3490
|
+
const charIndex = Math.floor(frame / 3);
|
|
3491
|
+
const showCursor = Math.floor(frame / 15) % 2 === 0;
|
|
3492
|
+
|
|
3493
|
+
<span>
|
|
3494
|
+
{text.slice(0, charIndex)}
|
|
3495
|
+
{showCursor && <span>|</span>}
|
|
3496
|
+
</span>
|
|
3555
3497
|
\`\`\`
|
|
3556
3498
|
|
|
3557
3499
|
---
|
|
3558
3500
|
|
|
3559
|
-
##
|
|
3501
|
+
## Vignette & Noise
|
|
3560
3502
|
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3503
|
+
\`\`\`tsx
|
|
3504
|
+
// Noise
|
|
3505
|
+
<AbsoluteFill style={{
|
|
3506
|
+
backgroundImage: 'url(/noise.png)',
|
|
3507
|
+
opacity: 0.03,
|
|
3508
|
+
mixBlendMode: 'overlay',
|
|
3509
|
+
}} />
|
|
3566
3510
|
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3511
|
+
// Vignette
|
|
3512
|
+
<AbsoluteFill style={{
|
|
3513
|
+
background: 'radial-gradient(ellipse at center, transparent 40%, rgba(0,0,0,0.8) 100%)',
|
|
3514
|
+
}} />
|
|
3571
3515
|
\`\`\`
|
|
3516
|
+
`
|
|
3517
|
+
},
|
|
3518
|
+
{
|
|
3519
|
+
filename: "advanced-techniques.md",
|
|
3520
|
+
content: `# Advanced Techniques
|
|
3572
3521
|
|
|
3573
|
-
|
|
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
|
|
3522
|
+
## Audio-Reactive
|
|
3579
3523
|
|
|
3580
|
-
|
|
3524
|
+
- **Kick:** \`scale(1.005)\` pulse
|
|
3525
|
+
- **Snare:** Trigger scene changes
|
|
3526
|
+
- **Hi-hats:** Cursor flicker, particle shimmer
|
|
3581
3527
|
|
|
3582
|
-
|
|
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)
|
|
3528
|
+
---
|
|
3586
3529
|
|
|
3587
|
-
|
|
3530
|
+
## Motion Blur (Fake)
|
|
3588
3531
|
|
|
3589
3532
|
\`\`\`tsx
|
|
3590
|
-
//
|
|
3591
|
-
const progress = spring({
|
|
3592
|
-
frame,
|
|
3593
|
-
fps,
|
|
3594
|
-
config: { mass: 2, damping: 20, stiffness: 100 },
|
|
3595
|
-
});
|
|
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]);
|
|
3533
|
+
// Trail: Render 3-4 copies with opacity 0.3, 1-frame delay
|
|
3601
3534
|
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
}}>
|
|
3605
|
-
{uiComponent}
|
|
3606
|
-
</div>
|
|
3535
|
+
// Or drop shadow for fast movement:
|
|
3536
|
+
filter: \`drop-shadow(\${velocityX * 0.5}px \${velocityY * 0.5}px 10px rgba(0,0,0,0.3))\`
|
|
3607
3537
|
\`\`\`
|
|
3608
3538
|
|
|
3609
|
-
|
|
3539
|
+
---
|
|
3610
3540
|
|
|
3611
|
-
|
|
3541
|
+
## 3D Perspective
|
|
3612
3542
|
|
|
3613
3543
|
\`\`\`tsx
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
}
|
|
3620
|
-
|
|
3621
|
-
|
|
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
|
-
})}
|
|
3544
|
+
<div style={{ perspective: '1000px' }}>
|
|
3545
|
+
<div style={{
|
|
3546
|
+
transform: 'rotateX(5deg) rotateY(10deg)',
|
|
3547
|
+
transformStyle: 'preserve-3d',
|
|
3548
|
+
}}>
|
|
3549
|
+
{/* Your UI */}
|
|
3550
|
+
</div>
|
|
3551
|
+
</div>
|
|
3683
3552
|
\`\`\`
|
|
3684
3553
|
|
|
3685
3554
|
---
|
|
3686
3555
|
|
|
3687
|
-
##
|
|
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:
|
|
3556
|
+
## Kinetic Typography
|
|
3694
3557
|
|
|
3558
|
+
### Masked Reveal
|
|
3695
3559
|
\`\`\`tsx
|
|
3696
3560
|
<div style={{ overflow: 'hidden', height: 80 }}>
|
|
3697
3561
|
<h1 style={{
|
|
@@ -3702,886 +3566,766 @@ Text rises from a floor:
|
|
|
3702
3566
|
</div>
|
|
3703
3567
|
\`\`\`
|
|
3704
3568
|
|
|
3705
|
-
###
|
|
3569
|
+
### Keyword Animation
|
|
3570
|
+
Animate keywords, not whole sentences.
|
|
3571
|
+
`
|
|
3572
|
+
},
|
|
3573
|
+
{
|
|
3574
|
+
filename: "remotion-config.md",
|
|
3575
|
+
content: `# Remotion Configuration
|
|
3706
3576
|
|
|
3707
|
-
|
|
3577
|
+
## FPS & Resolution
|
|
3708
3578
|
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
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
|
-
\`\`\`
|
|
3579
|
+
- **60 FPS mandatory** \u2014 30fps looks choppy
|
|
3580
|
+
- **1920\xD71080** Full HD
|
|
3581
|
+
- **Center:** \`{x: 960, y: 540}\`
|
|
3722
3582
|
|
|
3723
|
-
|
|
3583
|
+
---
|
|
3724
3584
|
|
|
3725
|
-
|
|
3585
|
+
## Timing
|
|
3726
3586
|
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
<span style={{ position: 'absolute', left: 2, color: '#0000FF', opacity: 0.5 }}>TEXT</span>
|
|
3733
|
-
</>
|
|
3734
|
-
)}
|
|
3735
|
-
\`\`\`
|
|
3587
|
+
- 1 second = 60 frames
|
|
3588
|
+
- Fast interaction = 15-20 frames
|
|
3589
|
+
- No scene > 8 seconds without action
|
|
3590
|
+
|
|
3591
|
+
---
|
|
3736
3592
|
|
|
3737
|
-
|
|
3593
|
+
## Entry-Action-Exit Structure
|
|
3738
3594
|
|
|
3739
|
-
|
|
3595
|
+
| Phase | Duration |
|
|
3596
|
+
|-------|----------|
|
|
3597
|
+
| Entry | 0.0s - 0.5s |
|
|
3598
|
+
| Action | 0.5s - (duration - 1s) |
|
|
3599
|
+
| Exit | last 1s |
|
|
3740
3600
|
|
|
3741
3601
|
---
|
|
3742
3602
|
|
|
3743
|
-
##
|
|
3603
|
+
## Font Loading
|
|
3744
3604
|
|
|
3745
|
-
|
|
3605
|
+
\`\`\`tsx
|
|
3606
|
+
const [handle] = useState(() => delayRender());
|
|
3746
3607
|
|
|
3747
|
-
|
|
3608
|
+
useEffect(() => {
|
|
3609
|
+
document.fonts.ready.then(() => {
|
|
3610
|
+
continueRender(handle);
|
|
3611
|
+
});
|
|
3612
|
+
}, [handle]);
|
|
3613
|
+
\`\`\`
|
|
3748
3614
|
|
|
3749
|
-
|
|
3750
|
-
const MovingBackground: React.FC = () => {
|
|
3751
|
-
const frame = useCurrentFrame();
|
|
3615
|
+
---
|
|
3752
3616
|
|
|
3753
|
-
|
|
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;
|
|
3617
|
+
## Zod Schema
|
|
3758
3618
|
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
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
|
-
};
|
|
3619
|
+
\`\`\`tsx
|
|
3620
|
+
export const SceneSchema = z.object({
|
|
3621
|
+
titleText: z.string(),
|
|
3622
|
+
buttonColor: z.string(),
|
|
3623
|
+
cursorPath: z.array(z.object({ x: z.number(), y: z.number() })),
|
|
3624
|
+
});
|
|
3789
3625
|
\`\`\`
|
|
3790
3626
|
|
|
3791
|
-
|
|
3627
|
+
---
|
|
3792
3628
|
|
|
3793
|
-
|
|
3794
|
-
// Noise texture overlay (5% opacity)
|
|
3795
|
-
<AbsoluteFill style={{
|
|
3796
|
-
backgroundImage: 'url(/noise.png)',
|
|
3797
|
-
opacity: 0.05,
|
|
3798
|
-
mixBlendMode: 'overlay',
|
|
3799
|
-
}} />
|
|
3629
|
+
## SaaS Video Kit Components
|
|
3800
3630
|
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3631
|
+
| Component | Purpose |
|
|
3632
|
+
|-----------|---------|
|
|
3633
|
+
| \`MockWindow\` | macOS window with traffic lights |
|
|
3634
|
+
| \`SmartCursor\` | Bezier curves + click physics |
|
|
3635
|
+
| \`NotificationToast\` | Slide in, wait, slide out |
|
|
3636
|
+
| \`TypingText\` | Typewriter with cursor |
|
|
3637
|
+
| \`Placeholder\` | For logos/icons |
|
|
3806
3638
|
|
|
3807
3639
|
---
|
|
3808
3640
|
|
|
3809
|
-
##
|
|
3641
|
+
## Code Rules
|
|
3810
3642
|
|
|
3811
|
-
|
|
3643
|
+
1. No \`transition: all 0.3s\` \u2014 use \`interpolate()\` or \`spring()\`
|
|
3644
|
+
2. Use \`AbsoluteFill\` for layout
|
|
3645
|
+
3. No magic numbers \u2014 extract to constants
|
|
3646
|
+
`
|
|
3647
|
+
},
|
|
3648
|
+
{
|
|
3649
|
+
filename: "elite-production.md",
|
|
3650
|
+
content: `# Elite Production
|
|
3812
3651
|
|
|
3813
|
-
|
|
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 |
|
|
3652
|
+
For Stripe/Apple/Linear quality.
|
|
3818
3653
|
|
|
3819
|
-
|
|
3654
|
+
---
|
|
3820
3655
|
|
|
3821
|
-
|
|
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 |
|
|
3656
|
+
## Global Lighting Engine
|
|
3827
3657
|
|
|
3828
|
-
|
|
3658
|
+
\`\`\`tsx
|
|
3659
|
+
const lightSource = { x: 0.2, y: -0.5 };
|
|
3660
|
+
const gradientAngle = Math.atan2(lightSource.y, lightSource.x) * (180 / Math.PI);
|
|
3829
3661
|
|
|
3830
|
-
|
|
3662
|
+
<button style={{
|
|
3663
|
+
background: \`linear-gradient(\${gradientAngle}deg, rgba(255,255,255,0.1) 0%, transparent 50%)\`,
|
|
3664
|
+
borderTop: '1px solid rgba(255,255,255,0.15)',
|
|
3665
|
+
boxShadow: \`\${-lightSource.x * 20}px \${-lightSource.y * 20}px 40px rgba(0,0,0,0.3)\`,
|
|
3666
|
+
}} />
|
|
3667
|
+
\`\`\`
|
|
3831
3668
|
|
|
3832
3669
|
---
|
|
3833
3670
|
|
|
3834
|
-
##
|
|
3835
|
-
|
|
3836
|
-
### The "Match Cut"
|
|
3671
|
+
## Noise & Dithering
|
|
3837
3672
|
|
|
3838
|
-
|
|
3673
|
+
Every background needs noise overlay (opacity 0.02-0.05). Prevents YouTube banding.
|
|
3839
3674
|
|
|
3840
|
-
|
|
3841
|
-
// Circle element scales 50x to wipe to next scene
|
|
3842
|
-
const circleScale = interpolate(frame, [MATCH_START, MATCH_END], [1, 50]);
|
|
3843
|
-
\`\`\`
|
|
3675
|
+
---
|
|
3844
3676
|
|
|
3845
|
-
|
|
3677
|
+
## React Three Fiber
|
|
3846
3678
|
|
|
3847
|
-
|
|
3679
|
+
For particles, 3D globes \u2014 use WebGL via \`@remotion/three\`, not CSS 3D.
|
|
3848
3680
|
|
|
3849
3681
|
\`\`\`tsx
|
|
3850
|
-
|
|
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
|
|
3682
|
+
import { ThreeCanvas } from '@remotion/three';
|
|
3854
3683
|
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
};
|
|
3684
|
+
<AbsoluteFill>
|
|
3685
|
+
<HtmlUI />
|
|
3686
|
+
<ThreeCanvas>
|
|
3687
|
+
<Particles />
|
|
3688
|
+
</ThreeCanvas>
|
|
3689
|
+
</AbsoluteFill>
|
|
3862
3690
|
\`\`\`
|
|
3863
3691
|
|
|
3864
|
-
|
|
3692
|
+
See \`threejs-*\` skills for implementation.
|
|
3865
3693
|
|
|
3866
|
-
|
|
3694
|
+
---
|
|
3867
3695
|
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
{ rotation: 0, x: 0 },
|
|
3872
|
-
{ rotation: 5, x: 20 },
|
|
3873
|
-
];
|
|
3696
|
+
## Virtual Camera Rig
|
|
3697
|
+
|
|
3698
|
+
Move camera, not elements:
|
|
3874
3699
|
|
|
3875
|
-
|
|
3876
|
-
|
|
3700
|
+
\`\`\`tsx
|
|
3701
|
+
const CameraProvider = ({ children }) => {
|
|
3702
|
+
const frame = useCurrentFrame();
|
|
3703
|
+
const panX = interpolate(frame, [0, 300], [0, -100]);
|
|
3704
|
+
const zoom = interpolate(frame, [0, 300], [1, 1.05]);
|
|
3877
3705
|
|
|
3878
3706
|
return (
|
|
3879
3707
|
<div style={{
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
zIndex: i,
|
|
3708
|
+
transform: \`translateX(\${panX}px) scale(\${zoom})\`,
|
|
3709
|
+
transformOrigin: 'center',
|
|
3883
3710
|
}}>
|
|
3884
|
-
|
|
3711
|
+
{children}
|
|
3885
3712
|
</div>
|
|
3886
3713
|
);
|
|
3887
|
-
}
|
|
3714
|
+
};
|
|
3888
3715
|
\`\`\`
|
|
3889
3716
|
|
|
3890
3717
|
---
|
|
3891
3718
|
|
|
3892
|
-
##
|
|
3719
|
+
## Motion Rules
|
|
3893
3720
|
|
|
3894
|
-
**
|
|
3895
|
-
**
|
|
3721
|
+
- **Overshoot:** Modal scales to 1.02, settles to 1.0
|
|
3722
|
+
- **Overlap:** Scene B starts 15 frames before Scene A ends
|
|
3723
|
+
`
|
|
3724
|
+
},
|
|
3725
|
+
{
|
|
3726
|
+
filename: "known-issues.md",
|
|
3727
|
+
content: `# Known Issues & Fixes
|
|
3896
3728
|
|
|
3897
|
-
|
|
3729
|
+
## 1. Music Ends Before Video Finishes
|
|
3898
3730
|
|
|
3899
|
-
|
|
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.
|
|
3731
|
+
**Problem:** Music duration is shorter than video duration, causing awkward silence at the end.
|
|
3903
3732
|
|
|
3904
|
-
|
|
3733
|
+
**Solution:** Loop music in Remotion using the \`loop\` prop:
|
|
3905
3734
|
|
|
3906
|
-
|
|
3907
|
-
|
|
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
|
|
3735
|
+
\`\`\`tsx
|
|
3736
|
+
import { Audio } from 'remotion';
|
|
3914
3737
|
|
|
3915
|
-
|
|
3738
|
+
<Audio src={musicSrc} volume={0.3} loop />
|
|
3739
|
+
\`\`\`
|
|
3916
3740
|
|
|
3917
|
-
|
|
3918
|
-
-
|
|
3919
|
-
-
|
|
3741
|
+
**How it works:**
|
|
3742
|
+
- Music automatically loops to fill video duration
|
|
3743
|
+
- Set volume to 0.3 (30% - less loud than voice)
|
|
3744
|
+
- Add fade out at the end for smooth ending
|
|
3920
3745
|
|
|
3921
|
-
|
|
3746
|
+
---
|
|
3922
3747
|
|
|
3923
|
-
|
|
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.
|
|
3748
|
+
## 2. Music Transitions Sound Abrupt
|
|
3926
3749
|
|
|
3927
|
-
|
|
3750
|
+
**Problem:** Music cuts harshly when scenes change or video ends.
|
|
3928
3751
|
|
|
3929
|
-
|
|
3752
|
+
**Fix in Remotion:**
|
|
3753
|
+
\`\`\`tsx
|
|
3754
|
+
import { interpolate, Audio } from 'remotion';
|
|
3930
3755
|
|
|
3931
|
-
|
|
3756
|
+
// Fade music in/out at scene boundaries
|
|
3757
|
+
const musicVolume = interpolate(
|
|
3758
|
+
frame,
|
|
3759
|
+
[0, 30, totalFrames - 60, totalFrames],
|
|
3760
|
+
[0, 0.3, 0.3, 0],
|
|
3761
|
+
{ extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }
|
|
3762
|
+
);
|
|
3932
3763
|
|
|
3933
|
-
|
|
3764
|
+
<Audio src={music} volume={musicVolume} />
|
|
3765
|
+
\`\`\`
|
|
3934
3766
|
|
|
3935
|
-
|
|
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).
|
|
3767
|
+
---
|
|
3938
3768
|
|
|
3939
|
-
|
|
3769
|
+
## 3. Scene Transitions Too Harsh
|
|
3940
3770
|
|
|
3941
|
-
|
|
3771
|
+
**Problem:** Scenes change abruptly without smooth transitions.
|
|
3942
3772
|
|
|
3773
|
+
**Fix:** Use \`@remotion/transitions\` with overlapping:
|
|
3943
3774
|
\`\`\`tsx
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3775
|
+
import { TransitionSeries, springTiming } from '@remotion/transitions';
|
|
3776
|
+
import { slide } from '@remotion/transitions/slide';
|
|
3777
|
+
import { fade } from '@remotion/transitions/fade';
|
|
3778
|
+
|
|
3779
|
+
<TransitionSeries>
|
|
3780
|
+
<TransitionSeries.Sequence durationInFrames={sceneA.frames}>
|
|
3781
|
+
<SceneA />
|
|
3782
|
+
</TransitionSeries.Sequence>
|
|
3783
|
+
<TransitionSeries.Transition
|
|
3784
|
+
presentation={slide({ direction: 'from-right' })}
|
|
3785
|
+
timing={springTiming({ config: { damping: 20 } })}
|
|
3786
|
+
/>
|
|
3787
|
+
<TransitionSeries.Sequence durationInFrames={sceneB.frames}>
|
|
3788
|
+
<SceneB />
|
|
3789
|
+
</TransitionSeries.Sequence>
|
|
3790
|
+
</TransitionSeries>
|
|
3950
3791
|
\`\`\`
|
|
3951
3792
|
|
|
3952
|
-
|
|
3793
|
+
---
|
|
3953
3794
|
|
|
3954
|
-
|
|
3795
|
+
## 4. Voiceover Lacks Energy
|
|
3955
3796
|
|
|
3956
|
-
|
|
3797
|
+
**Problem:** Voiceover sounds flat/monotone.
|
|
3957
3798
|
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3799
|
+
**Fix:** Pass \`voiceSettings\` in scenes JSON:
|
|
3800
|
+
\`\`\`json
|
|
3801
|
+
{
|
|
3802
|
+
"scenes": [...],
|
|
3803
|
+
"voice": "Kore",
|
|
3804
|
+
"voiceSettings": {
|
|
3805
|
+
"style": 0.6,
|
|
3806
|
+
"stability": 0.4,
|
|
3807
|
+
"speed": 0.95
|
|
3808
|
+
}
|
|
3809
|
+
}
|
|
3962
3810
|
\`\`\`
|
|
3963
3811
|
|
|
3964
|
-
|
|
3812
|
+
- \`style\`: 0.5-0.7 for more expressive delivery
|
|
3813
|
+
- \`stability\`: 0.3-0.5 for more variation
|
|
3814
|
+
- \`speed\`: 0.9-1.0 slightly slower = more impactful
|
|
3965
3815
|
|
|
3966
|
-
|
|
3816
|
+
---
|
|
3967
3817
|
|
|
3968
|
-
|
|
3969
|
-
import { TransitionWrapper } from './shared';
|
|
3818
|
+
## 5. Video Duration Mismatch
|
|
3970
3819
|
|
|
3971
|
-
|
|
3972
|
-
// enterFrom: 'left' | 'right' | 'bottom'
|
|
3820
|
+
**Problem:** Brief says 30-45s but video is 20s (because scene duration = voiceover duration).
|
|
3973
3821
|
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3822
|
+
**Fixes:**
|
|
3823
|
+
1. **Slow voice:** Use \`speed: 0.85\` in voiceSettings
|
|
3824
|
+
2. **Add padding in Remotion:** Hold last frame, add breathing room
|
|
3825
|
+
\`\`\`tsx
|
|
3826
|
+
// Add 30 frames (0.5s) padding after voiceover ends
|
|
3827
|
+
const paddedDuration = voiceoverFrames + 30;
|
|
3977
3828
|
\`\`\`
|
|
3829
|
+
3. **Brief should note:** "Duration based on voiceover length"
|
|
3830
|
+
|
|
3831
|
+
---
|
|
3832
|
+
|
|
3833
|
+
## 6. Not Using Project UI Components
|
|
3978
3834
|
|
|
3979
|
-
|
|
3835
|
+
**Problem:** Generic UI instead of pixel-perfect project components.
|
|
3980
3836
|
|
|
3981
|
-
**
|
|
3837
|
+
**Fix:** In Phase 1 Discovery:
|
|
3838
|
+
1. Find project's actual components (buttons, cards, modals, inputs)
|
|
3839
|
+
2. Copy their styles/structure into Remotion components
|
|
3840
|
+
3. Match colors, fonts, shadows, border-radius exactly
|
|
3982
3841
|
|
|
3983
3842
|
\`\`\`tsx
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
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
|
-
};
|
|
3843
|
+
// DON'T: Generic button
|
|
3844
|
+
<button style={{ background: 'blue' }}>Click</button>
|
|
3845
|
+
|
|
3846
|
+
// DO: Match project's actual button
|
|
3847
|
+
<button style={{
|
|
3848
|
+
background: 'linear-gradient(135deg, #6366f1, #8b5cf6)',
|
|
3849
|
+
borderRadius: 8,
|
|
3850
|
+
padding: '12px 24px',
|
|
3851
|
+
boxShadow: '0 4px 14px rgba(99, 102, 241, 0.4)',
|
|
3852
|
+
border: '1px solid rgba(255,255,255,0.1)',
|
|
3853
|
+
}}>Click</button>
|
|
4009
3854
|
\`\`\`
|
|
4010
3855
|
|
|
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
3856
|
---
|
|
4014
3857
|
|
|
4015
|
-
##
|
|
3858
|
+
## 7. Missing Physics & Lighting
|
|
4016
3859
|
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
3860
|
+
**Problem:** Video feels flat, no depth or motion.
|
|
3861
|
+
|
|
3862
|
+
**Checklist:**
|
|
3863
|
+
- [ ] Global light source defined (affects all shadows/gradients)
|
|
3864
|
+
- [ ] Camera rig with subtle drift/zoom
|
|
3865
|
+
- [ ] Spring physics on ALL entrances (no linear)
|
|
3866
|
+
- [ ] Staggered animations (never all at once)
|
|
3867
|
+
- [ ] Background orbs/particles moving
|
|
3868
|
+
- [ ] Noise overlay (opacity 0.02-0.05)
|
|
3869
|
+
- [ ] Vignette for depth
|
|
3870
|
+
`
|
|
3871
|
+
}
|
|
3872
|
+
];
|
|
4020
3873
|
|
|
3874
|
+
// src/commands/skill/generate-video-skill.ts
|
|
3875
|
+
function generateVideoSkillContent(context) {
|
|
3876
|
+
const { name, cmd: cmd2, displayName } = context;
|
|
3877
|
+
return `---
|
|
3878
|
+
name: ${name}-video
|
|
3879
|
+
description: Use when user asks to create videos (product demos, explainers, social content, promos). Handles video asset generation, Remotion implementation, and thumbnail embedding.
|
|
4021
3880
|
---
|
|
4022
3881
|
|
|
4023
|
-
|
|
3882
|
+
# ${displayName} Video Creation CLI
|
|
4024
3883
|
|
|
4025
|
-
|
|
3884
|
+
Create professional product videos directly from your terminal. The CLI generates AI-powered video assets (voiceover, music, images, stock videos) and provides tools for Remotion-based video production with React Three Fiber.
|
|
4026
3885
|
|
|
4027
|
-
|
|
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
|
|
3886
|
+
**Stack:** Remotion (React video framework) + React Three Fiber (R3F) + Three.js for 3D/WebGL, particles, shaders, lighting.
|
|
4037
3887
|
|
|
4038
|
-
|
|
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.
|
|
3888
|
+
We create **elite product videos** (Stripe, Apple, Linear quality) using physics-based animation, dynamic lighting, and pixel-perfect UI components rebuilt from the real project \u2014 never boring screenshots or static images.
|
|
4046
3889
|
|
|
4047
|
-
|
|
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
|
-
\`\`\`
|
|
3890
|
+
**Core Philosophy:** "Nothing sits still. Everything is physics-based. Every pixel breathes."
|
|
4059
3891
|
|
|
4060
|
-
|
|
3892
|
+
---
|
|
4061
3893
|
|
|
4062
|
-
|
|
4063
|
-
What you made:
|
|
4064
|
-
- "Lorem ipsum dolor sit amet..."
|
|
4065
|
-
- "Sample text here"
|
|
4066
|
-
- Generic placeholder content
|
|
3894
|
+
## CRITICAL: Professional Composition Rules
|
|
4067
3895
|
|
|
4068
|
-
|
|
4069
|
-
\`\`\`
|
|
3896
|
+
**These rules are MANDATORY for all marketing/product videos:**
|
|
4070
3897
|
|
|
4071
|
-
|
|
3898
|
+
### \u274C NEVER DO:
|
|
3899
|
+
1. **Walls of text** - No dense paragraphs or lists longer than 3 lines
|
|
3900
|
+
2. **Flying/floating cards** - No random floating animations across the screen
|
|
3901
|
+
3. **Stretched layouts** - No elements awkwardly stretched to fill space
|
|
3902
|
+
4. **Truncated text** - Never show "Text that gets cut off..."
|
|
3903
|
+
5. **Information overload** - Max 1-2 key points visible at once
|
|
3904
|
+
6. **Amateur motion** - No PowerPoint-style "fly in from left/right"
|
|
4072
3905
|
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
-
|
|
4076
|
-
-
|
|
4077
|
-
-
|
|
3906
|
+
### \u2705 ALWAYS DO:
|
|
3907
|
+
1. **Hierarchy first** - One clear focal point per scene (headline OR stat OR visual, not all)
|
|
3908
|
+
2. **Breathing room** - Generous whitespace (min 100px padding around elements)
|
|
3909
|
+
3. **Purposeful motion** - Cards appear with subtle spring (0-20px translateY), not fly across screen
|
|
3910
|
+
4. **Readable text** - Max 2-3 lines per card, 24px+ font size
|
|
3911
|
+
5. **Grid alignment** - Use invisible grid (3-column or 4-column layout)
|
|
3912
|
+
6. **Professional entrance** - Elements fade + slight translate (15px max), hold for 2-3s, then exit
|
|
4078
3913
|
|
|
4079
|
-
|
|
4080
|
-
\`\`\`
|
|
4081
|
-
|
|
4082
|
-
## FAILURE 4: Elements Just Appearing
|
|
3914
|
+
### Composition Examples:
|
|
4083
3915
|
|
|
3916
|
+
**\u274C BAD - Wall of Text:**
|
|
3917
|
+
\`\`\`tsx
|
|
3918
|
+
// DON'T: 10 bullet points crammed in a card
|
|
3919
|
+
<Card>
|
|
3920
|
+
<ul>
|
|
3921
|
+
{[...10items].map(item => <li>{item.longText}...</li>)}
|
|
3922
|
+
</ul>
|
|
3923
|
+
</Card>
|
|
4084
3924
|
\`\`\`
|
|
4085
|
-
What you made:
|
|
4086
|
-
- Frame 0: nothing
|
|
4087
|
-
- Frame 1: element is fully visible
|
|
4088
|
-
- No transition, no animation
|
|
4089
3925
|
|
|
4090
|
-
|
|
3926
|
+
**\u2705 GOOD - Single Focus:**
|
|
3927
|
+
\`\`\`tsx
|
|
3928
|
+
// DO: One headline, one supporting stat
|
|
3929
|
+
<AbsoluteFill style={{ alignItems: 'center', justifyContent: 'center' }}>
|
|
3930
|
+
<h1 style={{ fontSize: 72, marginBottom: 40 }}>12 hours wasted</h1>
|
|
3931
|
+
<p style={{ fontSize: 28, opacity: 0.7 }}>per week on manual tasks</p>
|
|
3932
|
+
</AbsoluteFill>
|
|
4091
3933
|
\`\`\`
|
|
4092
3934
|
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
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.
|
|
3935
|
+
**\u274C BAD - Flying Cards:**
|
|
3936
|
+
\`\`\`tsx
|
|
3937
|
+
// DON'T: Cards flying from random positions
|
|
3938
|
+
<Card style={{
|
|
3939
|
+
transform: \`translateX(\${interpolate(progress, [0,1], [-500, 0])}px)\` // Flies from left
|
|
3940
|
+
}} />
|
|
4105
3941
|
\`\`\`
|
|
4106
3942
|
|
|
4107
|
-
|
|
4108
|
-
|
|
3943
|
+
**\u2705 GOOD - Subtle Entrance:**
|
|
3944
|
+
\`\`\`tsx
|
|
3945
|
+
// DO: Gentle spring entrance with minimal movement
|
|
3946
|
+
const progress = spring({ frame: frame - startFrame, fps, config: { damping: 20, stiffness: 100 }});
|
|
3947
|
+
<Card style={{
|
|
3948
|
+
opacity: progress,
|
|
3949
|
+
transform: \`translateY(\${interpolate(progress, [0,1], [15, 0])}px)\` // Subtle 15px drop
|
|
3950
|
+
}} />
|
|
4109
3951
|
\`\`\`
|
|
4110
|
-
What you made:
|
|
4111
|
-
- Camera never moves
|
|
4112
|
-
- No drift, no zoom, no rotation
|
|
4113
|
-
- Feels like watching a screenshot
|
|
4114
3952
|
|
|
4115
|
-
|
|
4116
|
-
\`\`\`
|
|
3953
|
+
### Layout Grid System:
|
|
4117
3954
|
|
|
4118
|
-
|
|
3955
|
+
**Use 12-column grid (like Bootstrap):**
|
|
3956
|
+
\`\`\`tsx
|
|
3957
|
+
const GRID = {
|
|
3958
|
+
columns: 12,
|
|
3959
|
+
gutter: 40,
|
|
3960
|
+
padding: 120 // Edge padding
|
|
3961
|
+
};
|
|
4119
3962
|
|
|
3963
|
+
// Center 6 columns for main content
|
|
3964
|
+
const contentWidth = (1920 - (GRID.padding * 2) - (GRID.gutter * 5)) / 2;
|
|
4120
3965
|
\`\`\`
|
|
4121
|
-
What you made:
|
|
4122
|
-
- Cursor moves in straight lines
|
|
4123
|
-
- No overshoot on stops
|
|
4124
|
-
- Click has no feedback
|
|
4125
3966
|
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
filename: "parameterization.md",
|
|
4132
|
-
content: `# Parameterization (Critical)
|
|
3967
|
+
**Positioning anchors:**
|
|
3968
|
+
- **Top-left:** Brand logo, context (10% from edges)
|
|
3969
|
+
- **Center:** Primary headline/stat/demo (50% transform)
|
|
3970
|
+
- **Bottom:** CTA or tagline (10% from bottom)
|
|
3971
|
+
- **Never:** Random floating between these zones
|
|
4133
3972
|
|
|
4134
|
-
|
|
3973
|
+
---
|
|
4135
3974
|
|
|
4136
|
-
##
|
|
3975
|
+
## Prerequisites
|
|
4137
3976
|
|
|
4138
|
-
|
|
3977
|
+
Before using this skill, ensure you have:
|
|
4139
3978
|
|
|
4140
|
-
|
|
3979
|
+
1. **Load related skills:**
|
|
3980
|
+
\`\`\`
|
|
3981
|
+
remotion-best-practices
|
|
3982
|
+
threejs-fundamentals
|
|
3983
|
+
\`\`\`
|
|
4141
3984
|
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
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;
|
|
3985
|
+
2. **Authenticate:**
|
|
3986
|
+
\`\`\`bash
|
|
3987
|
+
${cmd2} login
|
|
3988
|
+
\`\`\`
|
|
4150
3989
|
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
3990
|
+
3. **Remotion installed** (if creating videos):
|
|
3991
|
+
\`\`\`bash
|
|
3992
|
+
pnpm install remotion @remotion/cli
|
|
3993
|
+
\`\`\`
|
|
4154
3994
|
|
|
4155
|
-
|
|
3995
|
+
---
|
|
4156
3996
|
|
|
4157
|
-
|
|
4158
|
-
// Calculate from keyframes, don't hardcode
|
|
4159
|
-
const sceneDuration = SCENE_END - SCENE_START;
|
|
4160
|
-
\`\`\`
|
|
3997
|
+
## Video Creation Workflow
|
|
4161
3998
|
|
|
4162
|
-
|
|
3999
|
+
### Phase 0: Load Skills (MANDATORY)
|
|
4163
4000
|
|
|
4164
|
-
|
|
4165
|
-
// If audio timestamp changes, only update one variable
|
|
4166
|
-
const VOICEOVER_START = 45;
|
|
4167
|
-
const VISUAL_CUE = VOICEOVER_START + 10; // Auto-adjusts
|
|
4001
|
+
Before ANY video work, invoke these skills:
|
|
4168
4002
|
\`\`\`
|
|
4169
|
-
|
|
4170
|
-
|
|
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;
|
|
4003
|
+
remotion-best-practices
|
|
4004
|
+
threejs-fundamentals
|
|
4177
4005
|
\`\`\`
|
|
4178
|
-
`
|
|
4179
|
-
},
|
|
4180
|
-
{
|
|
4181
|
-
filename: "layers.md",
|
|
4182
|
-
content: `# Layers & Composition
|
|
4183
4006
|
|
|
4184
|
-
|
|
4007
|
+
### Phase 1: Discovery
|
|
4185
4008
|
|
|
4186
|
-
|
|
4009
|
+
Explore current directory silently:
|
|
4010
|
+
- Understand what the product does (README, docs, code)
|
|
4011
|
+
- Find branding: logo, colors, fonts
|
|
4012
|
+
- Find UI components to copy into Remotion (buttons, cards, modals, etc.) \u2014 rebuild pixel-perfect, no screenshots
|
|
4187
4013
|
|
|
4188
|
-
|
|
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) |
|
|
4014
|
+
### Phase 2: Video Brief
|
|
4198
4015
|
|
|
4199
|
-
|
|
4016
|
+
Present a brief outline (scenes \u22648s each, duration, assets found) and get user approval before production.
|
|
4200
4017
|
|
|
4201
|
-
|
|
4202
|
-
<AbsoluteFill>
|
|
4203
|
-
{/* Background layer */}
|
|
4204
|
-
<MovingBackground />
|
|
4018
|
+
### Phase 3: Production
|
|
4205
4019
|
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
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
|
-
\`\`\`
|
|
4020
|
+
1. **Generate audio assets** - \`${cmd2} video create\` with scenes JSON
|
|
4021
|
+
- IMPORTANT: Music is generated LAST after all voiceover/audio to ensure exact duration match
|
|
4022
|
+
2. **Scaffold OUTSIDE project** - \`cd .. && ${cmd2} video init my-video\`
|
|
4023
|
+
3. **Copy assets + UI components** from project into video project
|
|
4024
|
+
4. **Implement** - follow rules below
|
|
4224
4025
|
|
|
4225
|
-
|
|
4026
|
+
### Phase 4: Render & Thumbnail (REQUIRED)
|
|
4226
4027
|
|
|
4227
|
-
|
|
4028
|
+
\`\`\`bash
|
|
4029
|
+
# 1. Render the video (with voiceover and music already included)
|
|
4030
|
+
pnpm exec remotion render FullVideo
|
|
4228
4031
|
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
<Scene1 />
|
|
4232
|
-
</Sequence>
|
|
4233
|
-
<Sequence from={80} durationInFrames={100}> {/* 20 frame overlap */}
|
|
4234
|
-
<Scene2 />
|
|
4235
|
-
</Sequence>
|
|
4032
|
+
# 2. ALWAYS embed thumbnail before delivering
|
|
4033
|
+
${cmd2} video thumbnail out/FullVideo.mp4 --frame 60
|
|
4236
4034
|
\`\`\`
|
|
4237
|
-
`
|
|
4238
|
-
},
|
|
4239
|
-
{
|
|
4240
|
-
filename: "project-based.md",
|
|
4241
|
-
content: `# Component Extraction (Critical for Product Videos)
|
|
4242
4035
|
|
|
4243
|
-
|
|
4036
|
+
**Note:** Remotion videos include per-scene voiceovers and background music baked in during render.
|
|
4244
4037
|
|
|
4245
|
-
|
|
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).
|
|
4038
|
+
---
|
|
4248
4039
|
|
|
4249
|
-
##
|
|
4040
|
+
## Asset Generation
|
|
4250
4041
|
|
|
4251
|
-
|
|
4042
|
+
Generate voiceover, music, and visual assets for each scene:
|
|
4252
4043
|
|
|
4253
4044
|
\`\`\`bash
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4045
|
+
cat <<SCENES | ${cmd2} video create --output ./public
|
|
4046
|
+
{
|
|
4047
|
+
"scenes": [
|
|
4048
|
+
{
|
|
4049
|
+
"name": "Hook",
|
|
4050
|
+
"script": "Watch how we transformed this complex workflow into a single click.",
|
|
4051
|
+
"imageQuery": "modern dashboard interface dark theme",
|
|
4052
|
+
"videoQuery": "abstract tech particles animation"
|
|
4053
|
+
},
|
|
4054
|
+
{
|
|
4055
|
+
"name": "Demo",
|
|
4056
|
+
"script": "Our AI analyzes your data in real-time, surfacing insights that matter.",
|
|
4057
|
+
"imageQuery": "data visualization charts analytics"
|
|
4058
|
+
},
|
|
4059
|
+
{
|
|
4060
|
+
"name": "CTA",
|
|
4061
|
+
"script": "Start your free trial today. No credit card required.",
|
|
4062
|
+
"imageQuery": "call to action button modern"
|
|
4063
|
+
}
|
|
4064
|
+
],
|
|
4065
|
+
"voice": "Kore",
|
|
4066
|
+
"voiceSettings": {
|
|
4067
|
+
"style": 0.6,
|
|
4068
|
+
"stability": 0.4,
|
|
4069
|
+
"speed": 0.95
|
|
4070
|
+
},
|
|
4071
|
+
"musicPrompt": "upbeat corporate, positive energy, modern synth"
|
|
4072
|
+
}
|
|
4073
|
+
SCENES
|
|
4258
4074
|
\`\`\`
|
|
4259
4075
|
|
|
4260
|
-
|
|
4076
|
+
**Output:**
|
|
4077
|
+
- \`public/audio/Hook.mp3\` - scene voiceovers
|
|
4078
|
+
- \`public/audio/music.mp3\` - background music (30s max)
|
|
4079
|
+
- \`public/video-manifest.json\` - timing and metadata
|
|
4080
|
+
- Stock images/videos (if requested)
|
|
4261
4081
|
|
|
4262
|
-
|
|
4082
|
+
---
|
|
4263
4083
|
|
|
4264
|
-
|
|
4084
|
+
## Core Video Rules
|
|
4265
4085
|
|
|
4266
|
-
|
|
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
|
-
}
|
|
4086
|
+
${VIDEO_RULE_CONTENTS.map((rule) => rule.content).join("\n\n---\n\n")}
|
|
4273
4087
|
|
|
4274
|
-
|
|
4275
|
-
export function PricingCard({ plan, progress, isHovered }) {
|
|
4276
|
-
// No useState, no API calls, no handlers
|
|
4277
|
-
// Just visual + animation props from Remotion
|
|
4088
|
+
## Useful Commands
|
|
4278
4089
|
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
<div style={{
|
|
4284
|
-
transform: \`perspective(1000px) rotateX(\${rotateX}deg)\`,
|
|
4285
|
-
// ... exact same visual styles as original
|
|
4286
|
-
}}>
|
|
4287
|
-
...
|
|
4288
|
-
</div>
|
|
4289
|
-
);
|
|
4290
|
-
}
|
|
4291
|
-
\`\`\`
|
|
4090
|
+
\`\`\`bash
|
|
4091
|
+
# Generate video assets
|
|
4092
|
+
${cmd2} video create < scenes.json
|
|
4093
|
+
cat scenes.json | ${cmd2} video create --output ./public
|
|
4292
4094
|
|
|
4293
|
-
|
|
4095
|
+
# Initialize Remotion project
|
|
4096
|
+
${cmd2} video init my-video
|
|
4294
4097
|
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
- Spring physics
|
|
4298
|
-
- Staggered children
|
|
4299
|
-
- Cursor interaction states
|
|
4098
|
+
# Embed thumbnail
|
|
4099
|
+
${cmd2} video thumbnail out/video.mp4 --frame 60
|
|
4300
4100
|
|
|
4301
|
-
|
|
4101
|
+
# Search for stock assets
|
|
4102
|
+
${cmd2} images search "mountain landscape" --limit 10
|
|
4103
|
+
${cmd2} videos search "ocean waves" --limit 5
|
|
4302
4104
|
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
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"
|
|
4105
|
+
# Generate audio
|
|
4106
|
+
${cmd2} audio generate "Your script here" --voice Kore
|
|
4107
|
+
${cmd2} music generate "upbeat corporate" --duration 30
|
|
4310
4108
|
\`\`\`
|
|
4311
4109
|
|
|
4312
|
-
|
|
4110
|
+
---
|
|
4313
4111
|
|
|
4314
|
-
|
|
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 |
|
|
4112
|
+
## Best Practices
|
|
4320
4113
|
|
|
4321
|
-
|
|
4114
|
+
1. **Keep scenes under 8 seconds** without cuts or major action
|
|
4115
|
+
2. **Use spring physics** for all animations, never linear
|
|
4116
|
+
3. **Rebuild UI components** in React/CSS, no screenshots
|
|
4117
|
+
4. **Test with thumbnail embedding** before delivering
|
|
4118
|
+
5. **Music volume at 30%** (30-40% less loud than voice)
|
|
4119
|
+
6. **Read all video rules** in Phase 0 before implementation
|
|
4322
4120
|
|
|
4323
|
-
|
|
4324
|
-
\u2705 Using exact project colors from tailwind config
|
|
4121
|
+
---
|
|
4325
4122
|
|
|
4326
|
-
|
|
4327
|
-
\u2705 Real content: "Unlimited projects", "Priority support"
|
|
4123
|
+
## Troubleshooting
|
|
4328
4124
|
|
|
4329
|
-
|
|
4330
|
-
|
|
4125
|
+
If you encounter issues:
|
|
4126
|
+
- Check authentication: \`${cmd2} whoami\`
|
|
4127
|
+
- Verify asset generation: check \`video-manifest.json\`
|
|
4128
|
+
- Voiceover flat: increase style (0.5-0.7), decrease stability (0.3-0.5)
|
|
4129
|
+
- Duration mismatch: adjust \`voiceSettings.speed\` or add padding in Remotion
|
|
4331
4130
|
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4131
|
+
For detailed troubleshooting, see "Known Issues" section above.
|
|
4132
|
+
`;
|
|
4133
|
+
}
|
|
4134
|
+
|
|
4135
|
+
// src/commands/skill/generate-presentation-skill.ts
|
|
4136
|
+
function generatePresentationSkillContent(context) {
|
|
4137
|
+
const { name, cmd: cmd2, displayName } = context;
|
|
4138
|
+
return `---
|
|
4139
|
+
name: ${name}-presentation
|
|
4140
|
+
description: Use when user asks to create presentations (slides, decks, pitch decks). Generates AI-powered presentations with structured content and professional design.
|
|
4141
|
+
---
|
|
4339
4142
|
|
|
4340
|
-
|
|
4143
|
+
# ${displayName} Presentation CLI
|
|
4341
4144
|
|
|
4342
|
-
|
|
4145
|
+
Create professional presentations (slides, decks, pitch decks) directly from your terminal. The CLI generates AI-powered slides from any context you provide - text, files, URLs, or piped content. Also supports searching for stock images and videos.
|
|
4343
4146
|
|
|
4344
4147
|
---
|
|
4345
4148
|
|
|
4346
|
-
##
|
|
4347
|
-
|
|
4348
|
-
You have 3 seconds before the thumb scrolls. Win or lose everything.
|
|
4149
|
+
## Authentication
|
|
4349
4150
|
|
|
4350
|
-
|
|
4151
|
+
\`\`\`bash
|
|
4152
|
+
# Login via OAuth
|
|
4153
|
+
${cmd2} login
|
|
4351
4154
|
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4155
|
+
# Or set API key
|
|
4156
|
+
export ${name.toUpperCase().replace(/-/g, "_")}_API_KEY="your-key-here"
|
|
4157
|
+
\`\`\`
|
|
4355
4158
|
|
|
4356
|
-
|
|
4159
|
+
---
|
|
4357
4160
|
|
|
4358
|
-
|
|
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 |
|
|
4161
|
+
## Creating Presentations
|
|
4364
4162
|
|
|
4365
|
-
###
|
|
4163
|
+
### From Text
|
|
4366
4164
|
|
|
4367
|
-
|
|
4368
|
-
|
|
4165
|
+
\`\`\`bash
|
|
4166
|
+
${cmd2} create "AI-powered product analytics platform"
|
|
4167
|
+
\`\`\`
|
|
4369
4168
|
|
|
4370
|
-
|
|
4169
|
+
### From File
|
|
4371
4170
|
|
|
4372
|
-
|
|
4171
|
+
\`\`\`bash
|
|
4172
|
+
${cmd2} create --file product-brief.md
|
|
4173
|
+
\`\`\`
|
|
4373
4174
|
|
|
4374
|
-
|
|
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 |
|
|
4175
|
+
### From URL
|
|
4380
4176
|
|
|
4381
|
-
|
|
4177
|
+
\`\`\`bash
|
|
4178
|
+
${cmd2} create --url https://company.com/product
|
|
4179
|
+
\`\`\`
|
|
4382
4180
|
|
|
4383
|
-
|
|
4181
|
+
### From Piped Content
|
|
4384
4182
|
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
| Smooth transitions | Jump cuts |
|
|
4390
|
-
| Color graded | Raw, slightly oversaturated |
|
|
4183
|
+
\`\`\`bash
|
|
4184
|
+
cat research.txt | ${cmd2} create
|
|
4185
|
+
pbpaste | ${cmd2} create
|
|
4186
|
+
\`\`\`
|
|
4391
4187
|
|
|
4392
|
-
###
|
|
4188
|
+
### Advanced Options
|
|
4393
4189
|
|
|
4394
|
-
\`\`\`
|
|
4395
|
-
|
|
4190
|
+
\`\`\`bash
|
|
4191
|
+
${cmd2} create "Topic" \\
|
|
4192
|
+
--slides 10 \\
|
|
4193
|
+
--style professional \\
|
|
4194
|
+
--branding my-brand \\
|
|
4195
|
+
--template minimal \\
|
|
4196
|
+
--output presentation.zip
|
|
4396
4197
|
\`\`\`
|
|
4397
4198
|
|
|
4398
4199
|
---
|
|
4399
4200
|
|
|
4400
|
-
##
|
|
4201
|
+
## Presentation Options
|
|
4401
4202
|
|
|
4402
|
-
|
|
4203
|
+
- **\`--slides <count>\`** - Number of slides (default: 8-12 based on content)
|
|
4204
|
+
- **\`--style <style>\`** - Presentation style: professional, creative, minimal, corporate
|
|
4205
|
+
- **\`--branding <name>\`** - Use saved branding profile
|
|
4206
|
+
- **\`--template <name>\`** - Design template to use
|
|
4207
|
+
- **\`--output <file>\`** - Export to file (.zip, .pptx, .pdf)
|
|
4208
|
+
- **\`--format <format>\`** - Output format: human, json, quiet
|
|
4403
4209
|
|
|
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
|
-
\`\`\`
|
|
4210
|
+
---
|
|
4418
4211
|
|
|
4419
|
-
|
|
4212
|
+
## Managing Presentations
|
|
4420
4213
|
|
|
4421
|
-
\`\`\`
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
left: 60, // Edge safety
|
|
4426
|
-
right: 120, // Like/Comment/Share icons
|
|
4427
|
-
};
|
|
4214
|
+
\`\`\`bash
|
|
4215
|
+
# List all presentations
|
|
4216
|
+
${cmd2} list
|
|
4217
|
+
${cmd2} list --format json
|
|
4428
4218
|
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
\`\`\`
|
|
4219
|
+
# Get presentation details
|
|
4220
|
+
${cmd2} get <id-or-slug>
|
|
4432
4221
|
|
|
4433
|
-
|
|
4222
|
+
# Export presentation
|
|
4223
|
+
${cmd2} export <id-or-slug> -o presentation.zip
|
|
4434
4224
|
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
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
|
-
);
|
|
4225
|
+
# Import presentation
|
|
4226
|
+
${cmd2} import ./presentation.zip
|
|
4227
|
+
|
|
4228
|
+
# Delete presentation
|
|
4229
|
+
${cmd2} delete <id-or-slug>
|
|
4455
4230
|
\`\`\`
|
|
4456
4231
|
|
|
4457
4232
|
---
|
|
4458
4233
|
|
|
4459
|
-
##
|
|
4234
|
+
## Branding Management
|
|
4460
4235
|
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
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
|
-
\`\`\`
|
|
4236
|
+
\`\`\`bash
|
|
4237
|
+
# List saved brands
|
|
4238
|
+
${cmd2} branding list
|
|
4474
4239
|
|
|
4475
|
-
|
|
4240
|
+
# Extract branding from website
|
|
4241
|
+
${cmd2} branding extract https://company.com
|
|
4476
4242
|
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
const currentIndex = Math.floor(frame / FRAMES_PER_ITEM);
|
|
4480
|
-
// Slam-in with spring animation for each item
|
|
4243
|
+
# Use branding in presentation
|
|
4244
|
+
${cmd2} create "Topic" --branding company-brand
|
|
4481
4245
|
\`\`\`
|
|
4482
4246
|
|
|
4483
|
-
|
|
4247
|
+
---
|
|
4484
4248
|
|
|
4485
|
-
|
|
4486
|
-
- Slow, deliberate cursor movements
|
|
4487
|
-
- Satisfying click feedback
|
|
4488
|
-
- Ken Burns zoom on details
|
|
4249
|
+
## Stock Asset Search
|
|
4489
4250
|
|
|
4490
|
-
|
|
4251
|
+
\`\`\`bash
|
|
4252
|
+
# Search for images
|
|
4253
|
+
${cmd2} images search "mountain landscape" --limit 10
|
|
4254
|
+
${cmd2} images search "business team" --format json
|
|
4491
4255
|
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
</div>
|
|
4256
|
+
# Search for videos
|
|
4257
|
+
${cmd2} videos search "ocean waves" --limit 5
|
|
4258
|
+
${cmd2} videos search "city timelapse" --orientation landscape
|
|
4496
4259
|
\`\`\`
|
|
4497
4260
|
|
|
4498
4261
|
---
|
|
4499
4262
|
|
|
4500
|
-
##
|
|
4263
|
+
## Best Practices
|
|
4501
4264
|
|
|
4502
|
-
|
|
4265
|
+
1. **Provide context** - More input = better presentations
|
|
4266
|
+
2. **Use branding** - Extract and apply brand consistency
|
|
4267
|
+
3. **Review content** - AI-generated content should be reviewed
|
|
4268
|
+
4. **Export for sharing** - Use \`--output\` to create shareable files
|
|
4269
|
+
5. **Iterate** - Regenerate specific slides if needed
|
|
4503
4270
|
|
|
4504
|
-
|
|
4505
|
-
import { createTikTokStyleCaptions } from "@remotion/captions";
|
|
4271
|
+
---
|
|
4506
4272
|
|
|
4507
|
-
|
|
4508
|
-
captions: transcriptCaptions,
|
|
4509
|
-
combineTokensWithinMilliseconds: 1200, // Higher = more words per page
|
|
4510
|
-
});
|
|
4511
|
-
\`\`\`
|
|
4273
|
+
## Troubleshooting
|
|
4512
4274
|
|
|
4513
|
-
|
|
4275
|
+
**Authentication Issues:**
|
|
4276
|
+
\`\`\`bash
|
|
4277
|
+
# Check current user
|
|
4278
|
+
${cmd2} whoami
|
|
4514
4279
|
|
|
4515
|
-
|
|
4516
|
-
{
|
|
4517
|
-
|
|
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
|
-
})}
|
|
4280
|
+
# Re-authenticate
|
|
4281
|
+
${cmd2} logout
|
|
4282
|
+
${cmd2} login
|
|
4529
4283
|
\`\`\`
|
|
4530
4284
|
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
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) |
|
|
4285
|
+
**Generation Failures:**
|
|
4286
|
+
- Ensure input is clear and has enough context
|
|
4287
|
+
- Try different styles or templates
|
|
4288
|
+
- Check API status and quotas
|
|
4542
4289
|
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
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" |
|
|
4290
|
+
**Export Issues:**
|
|
4291
|
+
- Verify output format is supported
|
|
4292
|
+
- Check file permissions in output directory
|
|
4293
|
+
- Ensure presentation ID is correct
|
|
4550
4294
|
|
|
4551
4295
|
---
|
|
4552
4296
|
|
|
4553
|
-
##
|
|
4554
|
-
|
|
4555
|
-
### Platform Specs
|
|
4297
|
+
## Examples
|
|
4556
4298
|
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
| Shorts | 9:16 | 1080x1920 | 15-60s | 30 |
|
|
4562
|
-
| YouTube | 16:9 | 1920x1080 | any | 30/60 |
|
|
4299
|
+
**Quick pitch deck:**
|
|
4300
|
+
\`\`\`bash
|
|
4301
|
+
${cmd2} create "SaaS analytics platform for e-commerce" --slides 8 --style professional
|
|
4302
|
+
\`\`\`
|
|
4563
4303
|
|
|
4564
|
-
|
|
4304
|
+
**From product brief:**
|
|
4305
|
+
\`\`\`bash
|
|
4306
|
+
${cmd2} create --file brief.md --branding acme --output pitch.zip
|
|
4307
|
+
\`\`\`
|
|
4565
4308
|
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4309
|
+
**Research presentation:**
|
|
4310
|
+
\`\`\`bash
|
|
4311
|
+
cat research-notes.txt | ${cmd2} create --slides 15 --style minimal
|
|
4312
|
+
\`\`\`
|
|
4313
|
+
`;
|
|
4314
|
+
}
|
|
4572
4315
|
|
|
4573
|
-
|
|
4316
|
+
// src/commands/skill/installer.ts
|
|
4317
|
+
import { mkdirSync, writeFileSync, existsSync as existsSync2, rmSync } from "fs";
|
|
4318
|
+
import { join, resolve as resolve4, relative } from "path";
|
|
4319
|
+
import { homedir } from "os";
|
|
4574
4320
|
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
`
|
|
4584
|
-
}
|
|
4321
|
+
// src/commands/skill/editors.ts
|
|
4322
|
+
var SUPPORTED_EDITORS = [
|
|
4323
|
+
{ name: "Claude Code", dir: ".claude" },
|
|
4324
|
+
{ name: "Cursor", dir: ".cursor" },
|
|
4325
|
+
{ name: "Codex", dir: ".codex" },
|
|
4326
|
+
{ name: "OpenCode", dir: ".opencode" },
|
|
4327
|
+
{ name: "Windsurf", dir: ".windsurf" },
|
|
4328
|
+
{ name: "Agent", dir: ".agent" }
|
|
4585
4329
|
];
|
|
4586
4330
|
|
|
4587
4331
|
// src/commands/skill/installer.ts
|
|
@@ -4594,27 +4338,10 @@ function validatePath(basePath, targetPath) {
|
|
|
4594
4338
|
}
|
|
4595
4339
|
return resolvedTarget;
|
|
4596
4340
|
}
|
|
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
4341
|
function installSkillToPath(skillPath, content) {
|
|
4608
4342
|
const skillFile = join(skillPath, "SKILL.md");
|
|
4609
4343
|
mkdirSync(skillPath, { recursive: true });
|
|
4610
4344
|
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
4345
|
}
|
|
4619
4346
|
function installSkill(skillName, content, options = {}) {
|
|
4620
4347
|
const result = {
|
|
@@ -4678,67 +4405,113 @@ function getSupportedEditorNames() {
|
|
|
4678
4405
|
}
|
|
4679
4406
|
|
|
4680
4407
|
// src/commands/skill/index.ts
|
|
4681
|
-
var
|
|
4408
|
+
var skillContext = {
|
|
4409
|
+
name: brand.name,
|
|
4410
|
+
cmd: brand.commands[0],
|
|
4411
|
+
displayName: brand.displayName
|
|
4412
|
+
};
|
|
4413
|
+
var skillCommand = new Command14("skill").description(`Manage ${brand.displayName} skills for AI coding assistants`).addHelpText(
|
|
4682
4414
|
"after",
|
|
4683
4415
|
`
|
|
4684
4416
|
${chalk12.bold("Examples:")}
|
|
4685
|
-
${chalk12.gray("# Install skill
|
|
4686
|
-
$ ${brand.
|
|
4417
|
+
${chalk12.gray("# Install video skill")}
|
|
4418
|
+
$ ${brand.commands[0]} skill install video
|
|
4419
|
+
|
|
4420
|
+
${chalk12.gray("# Install presentation skill")}
|
|
4421
|
+
$ ${brand.commands[0]} skill install presentation
|
|
4422
|
+
|
|
4423
|
+
${chalk12.gray("# Install both skills")}
|
|
4424
|
+
$ ${brand.commands[0]} skill install
|
|
4687
4425
|
|
|
4688
4426
|
${chalk12.gray("# Install to specific directory")}
|
|
4689
|
-
$ ${brand.
|
|
4427
|
+
$ ${brand.commands[0]} skill install video --dir ~/.claude
|
|
4690
4428
|
|
|
4691
4429
|
${chalk12.gray("# Show skill content")}
|
|
4692
|
-
$ ${brand.
|
|
4430
|
+
$ ${brand.commands[0]} skill show video
|
|
4693
4431
|
`
|
|
4694
4432
|
);
|
|
4695
|
-
skillCommand.command("install").description(`Install
|
|
4696
|
-
const
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
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(", "));
|
|
4433
|
+
skillCommand.command("install").description(`Install ${brand.displayName} skills for AI coding assistants`).argument("[type]", "Skill type: video, presentation, or omit for both").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 (type, options) => {
|
|
4434
|
+
const skillsToInstall = [];
|
|
4435
|
+
if (!type || type === "video") {
|
|
4436
|
+
skillsToInstall.push({
|
|
4437
|
+
name: `${brand.name}-video`,
|
|
4438
|
+
content: generateVideoSkillContent(skillContext)
|
|
4439
|
+
});
|
|
4707
4440
|
}
|
|
4708
|
-
if (
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4441
|
+
if (!type || type === "presentation") {
|
|
4442
|
+
skillsToInstall.push({
|
|
4443
|
+
name: `${brand.name}-presentation`,
|
|
4444
|
+
content: generatePresentationSkillContent(skillContext)
|
|
4445
|
+
});
|
|
4712
4446
|
}
|
|
4713
|
-
if (
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
error(err);
|
|
4717
|
-
}
|
|
4447
|
+
if (type && type !== "video" && type !== "presentation") {
|
|
4448
|
+
error(`Invalid skill type: ${type}. Must be "video" or "presentation"`);
|
|
4449
|
+
process.exit(1);
|
|
4718
4450
|
}
|
|
4719
|
-
|
|
4720
|
-
|
|
4451
|
+
console.log();
|
|
4452
|
+
for (const skill of skillsToInstall) {
|
|
4453
|
+
info(`Installing ${skill.name}...`);
|
|
4454
|
+
const result = installSkill(skill.name, skill.content, {
|
|
4455
|
+
dir: options.dir,
|
|
4456
|
+
local: options.local,
|
|
4457
|
+
force: options.force
|
|
4458
|
+
});
|
|
4459
|
+
if (result.installed.length > 0) {
|
|
4460
|
+
success(`${skill.name} installed successfully`);
|
|
4461
|
+
keyValue(" Installed to", result.installed.join(", "));
|
|
4462
|
+
}
|
|
4463
|
+
if (result.skipped.length > 0) {
|
|
4464
|
+
info(` Skipped (already exists): ${result.skipped.join(", ")}`);
|
|
4465
|
+
console.log(chalk12.gray(" Use --force to overwrite"));
|
|
4466
|
+
}
|
|
4467
|
+
if (result.errors.length > 0) {
|
|
4468
|
+
for (const err of result.errors) {
|
|
4469
|
+
error(` ${err}`);
|
|
4470
|
+
}
|
|
4471
|
+
}
|
|
4472
|
+
if (result.installed.length === 0 && result.skipped.length === 0 && result.errors.length === 0) {
|
|
4473
|
+
info(" No supported AI coding assistants detected");
|
|
4474
|
+
console.log(chalk12.gray(" Supported editors: " + getSupportedEditorNames().join(", ")));
|
|
4475
|
+
console.log(chalk12.gray(" Use --dir <path> to install to a specific directory"));
|
|
4476
|
+
}
|
|
4721
4477
|
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
4478
|
}
|
|
4725
|
-
console.log();
|
|
4726
4479
|
});
|
|
4727
|
-
skillCommand.command("show").description("Display
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
console.log();
|
|
4733
|
-
if (result.removed.length > 0) {
|
|
4734
|
-
success("Skill uninstalled");
|
|
4735
|
-
keyValue("Removed from", result.removed.join(", "));
|
|
4480
|
+
skillCommand.command("show").description("Display skill content").argument("[type]", "Skill type: video or presentation (default: video)").action((type = "video") => {
|
|
4481
|
+
if (type === "video") {
|
|
4482
|
+
console.log(generateVideoSkillContent(skillContext));
|
|
4483
|
+
} else if (type === "presentation") {
|
|
4484
|
+
console.log(generatePresentationSkillContent(skillContext));
|
|
4736
4485
|
} else {
|
|
4737
|
-
|
|
4486
|
+
error(`Invalid skill type: ${type}. Must be "video" or "presentation"`);
|
|
4487
|
+
process.exit(1);
|
|
4488
|
+
}
|
|
4489
|
+
});
|
|
4490
|
+
skillCommand.command("uninstall").description(`Remove ${brand.displayName} skills from AI coding assistants`).argument("[type]", "Skill type: video, presentation, or omit for both").option("-g, --global", "Uninstall globally (from home directory)", true).option("-l, --local", "Uninstall locally (from current directory)").action(async (type, options) => {
|
|
4491
|
+
const skillsToRemove = [];
|
|
4492
|
+
if (!type || type === "video") {
|
|
4493
|
+
skillsToRemove.push(`${brand.name}-video`);
|
|
4738
4494
|
}
|
|
4739
|
-
if (
|
|
4740
|
-
|
|
4741
|
-
|
|
4495
|
+
if (!type || type === "presentation") {
|
|
4496
|
+
skillsToRemove.push(`${brand.name}-presentation`);
|
|
4497
|
+
}
|
|
4498
|
+
if (type && type !== "video" && type !== "presentation") {
|
|
4499
|
+
error(`Invalid skill type: ${type}. Must be "video" or "presentation"`);
|
|
4500
|
+
process.exit(1);
|
|
4501
|
+
}
|
|
4502
|
+
console.log();
|
|
4503
|
+
for (const skillName of skillsToRemove) {
|
|
4504
|
+
const result = uninstallSkill(skillName, { local: options.local });
|
|
4505
|
+
if (result.removed.length > 0) {
|
|
4506
|
+
success(`${skillName} uninstalled`);
|
|
4507
|
+
keyValue(" Removed from", result.removed.join(", "));
|
|
4508
|
+
} else {
|
|
4509
|
+
info(` ${skillName} not found`);
|
|
4510
|
+
}
|
|
4511
|
+
if (result.errors.length > 0) {
|
|
4512
|
+
for (const err of result.errors) {
|
|
4513
|
+
warn(` Failed to remove: ${err}`);
|
|
4514
|
+
}
|
|
4742
4515
|
}
|
|
4743
4516
|
}
|
|
4744
4517
|
console.log();
|
|
@@ -5000,7 +4773,7 @@ async function downloadFile2(url, outputPath) {
|
|
|
5000
4773
|
const buffer = await response.arrayBuffer();
|
|
5001
4774
|
await writeFile4(outputPath, Buffer.from(buffer));
|
|
5002
4775
|
}
|
|
5003
|
-
var mixCommand = new Command17("create").description("Mix audio tracks into a video").requiredOption("--video <url>", "Input video file/URL").option("--music <url>", "Background music file/URL").option("--voice <url>", "Voiceover file/URL").option("--music-volume <percent>", "Music volume 0-100", "
|
|
4776
|
+
var mixCommand = new Command17("create").description("Mix audio tracks into a video (music will loop to match video duration)").requiredOption("--video <url>", "Input video file/URL").option("--music <url>", "Background music file/URL (will loop if shorter than video)").option("--voice <url>", "Voiceover file/URL").option("--music-volume <percent>", "Music volume 0-100 (default: 30, recommended for mix with voice)", "30").option("--voice-volume <percent>", "Voice volume 0-100", "100").option("-o, --output <path>", "Output file path").option("--no-wait", "Do not wait for completion").option("-f, --format <format>", "Output format: human, json, quiet", "human").action(async (options) => {
|
|
5004
4777
|
if (!options.music && !options.voice) {
|
|
5005
4778
|
error("At least one of --music or --voice must be provided");
|
|
5006
4779
|
process.exit(EXIT_CODES.INVALID_INPUT);
|
|
@@ -5169,9 +4942,10 @@ init_output();
|
|
|
5169
4942
|
init_types();
|
|
5170
4943
|
import { Command as Command19 } from "commander";
|
|
5171
4944
|
import ora12 from "ora";
|
|
5172
|
-
import { mkdir, writeFile as writeFile5, readFile as readFile2, access, rm } from "fs/promises";
|
|
4945
|
+
import { mkdir, writeFile as writeFile5, readFile as readFile2, access, rm, cp } from "fs/promises";
|
|
5173
4946
|
import { join as join2, resolve as resolve5 } from "path";
|
|
5174
4947
|
import { execSync, spawn } from "child_process";
|
|
4948
|
+
import ffmpegPath from "ffmpeg-static";
|
|
5175
4949
|
var DEFAULT_TEMPLATE = "inizio-inc/remotion-composition";
|
|
5176
4950
|
var DEFAULT_FPS = 30;
|
|
5177
4951
|
function parseScriptIntoSections(script) {
|
|
@@ -5310,7 +5084,7 @@ function getExtension(url) {
|
|
|
5310
5084
|
}
|
|
5311
5085
|
return "jpg";
|
|
5312
5086
|
}
|
|
5313
|
-
var
|
|
5087
|
+
var createCommand2 = 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) => {
|
|
5314
5088
|
const format = options.format;
|
|
5315
5089
|
const spinner = format === "human" ? ora12("Initializing...").start() : null;
|
|
5316
5090
|
try {
|
|
@@ -5362,7 +5136,10 @@ var createCommand3 = new Command19("create").description("Create video assets (v
|
|
|
5362
5136
|
if (spinner) spinner.text = `[${scene.name}] Generating speech...`;
|
|
5363
5137
|
const ttsResult = await generateSpeech({
|
|
5364
5138
|
text: scene.script,
|
|
5365
|
-
options: {
|
|
5139
|
+
options: {
|
|
5140
|
+
voice,
|
|
5141
|
+
voiceSettings: scenesInput.voiceSettings
|
|
5142
|
+
}
|
|
5366
5143
|
});
|
|
5367
5144
|
const audioPath = join2(audioDir, `${filename}.${ttsResult.format}`);
|
|
5368
5145
|
await writeFile5(audioPath, ttsResult.audioData);
|
|
@@ -5417,7 +5194,7 @@ var createCommand3 = new Command19("create").description("Create video assets (v
|
|
|
5417
5194
|
try {
|
|
5418
5195
|
const videoResults = await searchVideos({
|
|
5419
5196
|
query: scene.videoQuery,
|
|
5420
|
-
options: { maxResults: 1 }
|
|
5197
|
+
options: { maxResults: 1, license: "free" }
|
|
5421
5198
|
});
|
|
5422
5199
|
const vids = videoResults.data.results.flatMap((r) => r.results);
|
|
5423
5200
|
totalCost += videoResults.data.totalCost;
|
|
@@ -5502,7 +5279,12 @@ var createCommand3 = new Command19("create").description("Create video assets (v
|
|
|
5502
5279
|
spinner?.start();
|
|
5503
5280
|
}
|
|
5504
5281
|
}
|
|
5505
|
-
const musicDuration = Math.min(30, Math.ceil(totalDuration)
|
|
5282
|
+
const musicDuration = Math.min(30, Math.ceil(totalDuration));
|
|
5283
|
+
console.log(`[Music Generation] Requesting music:`, {
|
|
5284
|
+
prompt: musicPrompt,
|
|
5285
|
+
requestedDuration: musicDuration,
|
|
5286
|
+
totalAudioDuration: totalDuration
|
|
5287
|
+
});
|
|
5506
5288
|
if (spinner) spinner.text = "Generating music...";
|
|
5507
5289
|
let musicResult = await generateMusic({
|
|
5508
5290
|
prompt: musicPrompt,
|
|
@@ -5526,15 +5308,28 @@ var createCommand3 = new Command19("create").description("Create video assets (v
|
|
|
5526
5308
|
await downloadFile3(musicResult.audioUrl, musicPath);
|
|
5527
5309
|
}
|
|
5528
5310
|
totalCost += musicResult.cost || 0;
|
|
5311
|
+
const actualMusicDuration = musicResult.duration || musicDuration;
|
|
5312
|
+
console.log(`[Music Generation] Received music:`, {
|
|
5313
|
+
requestedDuration: musicDuration,
|
|
5314
|
+
returnedDuration: musicResult.duration,
|
|
5315
|
+
actualUsedDuration: actualMusicDuration,
|
|
5316
|
+
totalAudioDuration: totalDuration,
|
|
5317
|
+
difference: actualMusicDuration - totalDuration,
|
|
5318
|
+
audioUrl: musicResult.audioUrl?.substring(0, 50) + "..."
|
|
5319
|
+
});
|
|
5529
5320
|
const musicInfo = {
|
|
5530
5321
|
path: "audio/music.mp3",
|
|
5531
|
-
duration:
|
|
5322
|
+
duration: actualMusicDuration,
|
|
5532
5323
|
prompt: musicPrompt,
|
|
5533
5324
|
cost: musicResult.cost || 0
|
|
5534
5325
|
};
|
|
5535
5326
|
if (format === "human") {
|
|
5536
5327
|
spinner?.stop();
|
|
5537
5328
|
success(`Music: ${musicPath} (${musicInfo.duration}s)`);
|
|
5329
|
+
if (actualMusicDuration < totalDuration) {
|
|
5330
|
+
warn(`Music duration (${actualMusicDuration.toFixed(1)}s) is shorter than video duration (${totalDuration.toFixed(1)}s).`);
|
|
5331
|
+
warn(`Consider using audio looping or extending music in Remotion.`);
|
|
5332
|
+
}
|
|
5538
5333
|
spinner?.start();
|
|
5539
5334
|
}
|
|
5540
5335
|
if (spinner) spinner.text = "Writing manifest...";
|
|
@@ -5582,7 +5377,7 @@ var createCommand3 = new Command19("create").description("Create video assets (v
|
|
|
5582
5377
|
process.exit(EXIT_CODES.GENERAL_ERROR);
|
|
5583
5378
|
}
|
|
5584
5379
|
});
|
|
5585
|
-
var searchCommand2 = new Command19("search").description("Search for stock videos").argument("<query>", "Search query").option("-n, --max-results <count>", "Maximum number of results", "10").option("-o, --orientation <type>", "Video orientation: landscape, portrait, square, any", "any").option("-l, --license <type>", "License type: free, premium, any", "
|
|
5380
|
+
var searchCommand2 = new Command19("search").description("Search for stock videos").argument("<query>", "Search query").option("-n, --max-results <count>", "Maximum number of results", "10").option("-o, --orientation <type>", "Video orientation: landscape, portrait, square, any", "any").option("-l, --license <type>", "License type: free, premium, any", "free").option("-f, --format <format>", "Output format: human, json, quiet", "human").action(async (query, options) => {
|
|
5586
5381
|
const { maxResults, orientation, license, format } = options;
|
|
5587
5382
|
const spinner = format === "human" ? ora12("Searching for videos...").start() : null;
|
|
5588
5383
|
try {
|
|
@@ -5751,10 +5546,147 @@ var initCommand = new Command19("init").description("Create a new Remotion video
|
|
|
5751
5546
|
process.exit(EXIT_CODES.GENERAL_ERROR);
|
|
5752
5547
|
}
|
|
5753
5548
|
});
|
|
5754
|
-
|
|
5549
|
+
function getFfmpegPath() {
|
|
5550
|
+
if (!ffmpegPath) {
|
|
5551
|
+
throw new Error("ffmpeg-static binary not found. Try reinstalling the CLI.");
|
|
5552
|
+
}
|
|
5553
|
+
return ffmpegPath;
|
|
5554
|
+
}
|
|
5555
|
+
var thumbnailCommand = new Command19("thumbnail").description("Embed a thumbnail/poster image into a video file for preview in Slack, Twitter, etc.").argument("<video>", "Video file to add thumbnail to (e.g., out/video.mp4)").option("-i, --image <path>", "Thumbnail image to embed (if not provided, extracts from video)").option("-f, --frame <number>", "Frame number to extract as thumbnail (default: 30)", "30").option("-c, --composition <id>", "Remotion composition to extract frame from (uses Remotion still)").option("-o, --output <path>", "Output video path (default: overwrites input)").option("--json", "Output as JSON").option("-q, --quiet", "Only output the file path").action(async (videoPath, options) => {
|
|
5556
|
+
const format = options.json ? "json" : options.quiet ? "quiet" : "human";
|
|
5557
|
+
const spinner = format === "human" ? ora12("Processing...").start() : null;
|
|
5558
|
+
try {
|
|
5559
|
+
let ffmpeg;
|
|
5560
|
+
try {
|
|
5561
|
+
ffmpeg = getFfmpegPath();
|
|
5562
|
+
} catch (err) {
|
|
5563
|
+
spinner?.stop();
|
|
5564
|
+
error(err instanceof Error ? err.message : "ffmpeg not available");
|
|
5565
|
+
process.exit(EXIT_CODES.GENERAL_ERROR);
|
|
5566
|
+
}
|
|
5567
|
+
const videoFullPath = resolve5(process.cwd(), videoPath);
|
|
5568
|
+
try {
|
|
5569
|
+
await access(videoFullPath);
|
|
5570
|
+
} catch {
|
|
5571
|
+
spinner?.stop();
|
|
5572
|
+
error(`Video file not found: ${videoPath}`);
|
|
5573
|
+
process.exit(EXIT_CODES.INVALID_INPUT);
|
|
5574
|
+
}
|
|
5575
|
+
const frameNum = parseInt(options.frame, 10);
|
|
5576
|
+
if (isNaN(frameNum) || frameNum < 0) {
|
|
5577
|
+
spinner?.stop();
|
|
5578
|
+
error("Invalid frame number. Must be a non-negative integer.");
|
|
5579
|
+
process.exit(EXIT_CODES.INVALID_INPUT);
|
|
5580
|
+
}
|
|
5581
|
+
let thumbnailPath = options.image;
|
|
5582
|
+
let tempThumbnail = false;
|
|
5583
|
+
if (!thumbnailPath) {
|
|
5584
|
+
const tempDir = join2(process.cwd(), ".tmp-thumbnail");
|
|
5585
|
+
await mkdir(tempDir, { recursive: true });
|
|
5586
|
+
thumbnailPath = join2(tempDir, "thumb.png");
|
|
5587
|
+
tempThumbnail = true;
|
|
5588
|
+
if (options.composition) {
|
|
5589
|
+
if (spinner) spinner.text = `Extracting frame ${frameNum} from ${options.composition}...`;
|
|
5590
|
+
const args = [
|
|
5591
|
+
"exec",
|
|
5592
|
+
"remotion",
|
|
5593
|
+
"still",
|
|
5594
|
+
options.composition,
|
|
5595
|
+
thumbnailPath,
|
|
5596
|
+
`--frame=${frameNum}`
|
|
5597
|
+
];
|
|
5598
|
+
try {
|
|
5599
|
+
execSync(`pnpm ${args.join(" ")}`, {
|
|
5600
|
+
stdio: "pipe",
|
|
5601
|
+
cwd: process.cwd()
|
|
5602
|
+
});
|
|
5603
|
+
} catch (err) {
|
|
5604
|
+
spinner?.stop();
|
|
5605
|
+
error(`Failed to extract frame from composition: ${err instanceof Error ? err.message : "Unknown error"}`);
|
|
5606
|
+
process.exit(EXIT_CODES.GENERAL_ERROR);
|
|
5607
|
+
}
|
|
5608
|
+
} else {
|
|
5609
|
+
if (spinner) spinner.text = `Extracting frame ${frameNum} from video...`;
|
|
5610
|
+
try {
|
|
5611
|
+
execSync(
|
|
5612
|
+
`"${ffmpeg}" -y -i "${videoFullPath}" -vf "select=eq(n\\,${frameNum})" -vframes 1 "${thumbnailPath}"`,
|
|
5613
|
+
{ stdio: "pipe" }
|
|
5614
|
+
);
|
|
5615
|
+
} catch (err) {
|
|
5616
|
+
spinner?.stop();
|
|
5617
|
+
error(`Failed to extract frame from video: ${err instanceof Error ? err.message : "Unknown error"}`);
|
|
5618
|
+
process.exit(EXIT_CODES.GENERAL_ERROR);
|
|
5619
|
+
}
|
|
5620
|
+
}
|
|
5621
|
+
} else {
|
|
5622
|
+
thumbnailPath = resolve5(process.cwd(), thumbnailPath);
|
|
5623
|
+
try {
|
|
5624
|
+
await access(thumbnailPath);
|
|
5625
|
+
} catch {
|
|
5626
|
+
spinner?.stop();
|
|
5627
|
+
error(`Thumbnail image not found: ${options.image}`);
|
|
5628
|
+
process.exit(EXIT_CODES.INVALID_INPUT);
|
|
5629
|
+
}
|
|
5630
|
+
}
|
|
5631
|
+
const outputPath = options.output ? resolve5(process.cwd(), options.output) : videoFullPath;
|
|
5632
|
+
const needsTempOutput = outputPath === videoFullPath;
|
|
5633
|
+
const tempOutput = needsTempOutput ? videoFullPath.replace(/\.mp4$/, ".thumb-temp.mp4") : outputPath;
|
|
5634
|
+
if (spinner) spinner.text = "Embedding thumbnail into video...";
|
|
5635
|
+
try {
|
|
5636
|
+
execSync(
|
|
5637
|
+
`"${ffmpeg}" -y -i "${videoFullPath}" -i "${thumbnailPath}" -map 0 -map 1 -c copy -disposition:v:1 attached_pic "${tempOutput}"`,
|
|
5638
|
+
{ stdio: "pipe" }
|
|
5639
|
+
);
|
|
5640
|
+
if (needsTempOutput) {
|
|
5641
|
+
await rm(videoFullPath);
|
|
5642
|
+
await cp(tempOutput, videoFullPath);
|
|
5643
|
+
await rm(tempOutput);
|
|
5644
|
+
}
|
|
5645
|
+
} catch (err) {
|
|
5646
|
+
spinner?.stop();
|
|
5647
|
+
error(`Failed to embed thumbnail: ${err instanceof Error ? err.message : "Unknown error"}`);
|
|
5648
|
+
process.exit(EXIT_CODES.GENERAL_ERROR);
|
|
5649
|
+
}
|
|
5650
|
+
if (tempThumbnail) {
|
|
5651
|
+
try {
|
|
5652
|
+
await rm(join2(process.cwd(), ".tmp-thumbnail"), { recursive: true });
|
|
5653
|
+
} catch {
|
|
5654
|
+
}
|
|
5655
|
+
}
|
|
5656
|
+
spinner?.stop();
|
|
5657
|
+
const finalOutput = options.output || videoPath;
|
|
5658
|
+
if (format === "json") {
|
|
5659
|
+
printJson({
|
|
5660
|
+
video: finalOutput,
|
|
5661
|
+
thumbnail: options.image || `frame ${frameNum}`,
|
|
5662
|
+
composition: options.composition || null
|
|
5663
|
+
});
|
|
5664
|
+
return;
|
|
5665
|
+
}
|
|
5666
|
+
if (format === "quiet") {
|
|
5667
|
+
console.log(resolve5(process.cwd(), finalOutput));
|
|
5668
|
+
return;
|
|
5669
|
+
}
|
|
5670
|
+
console.log();
|
|
5671
|
+
success(`Thumbnail embedded: ${finalOutput}`);
|
|
5672
|
+
if (options.image) {
|
|
5673
|
+
keyValue("Thumbnail", options.image);
|
|
5674
|
+
} else if (options.composition) {
|
|
5675
|
+
keyValue("Source", `${options.composition} frame ${frameNum}`);
|
|
5676
|
+
} else {
|
|
5677
|
+
keyValue("Source", `Video frame ${frameNum}`);
|
|
5678
|
+
}
|
|
5679
|
+
console.log();
|
|
5680
|
+
} catch (err) {
|
|
5681
|
+
spinner?.stop();
|
|
5682
|
+
error(err instanceof Error ? err.message : "Failed to process thumbnail");
|
|
5683
|
+
process.exit(EXIT_CODES.GENERAL_ERROR);
|
|
5684
|
+
}
|
|
5685
|
+
});
|
|
5686
|
+
var videoCommand = new Command19("video").description("Video asset generation commands").addCommand(initCommand).addCommand(createCommand2).addCommand(searchCommand2).addCommand(thumbnailCommand);
|
|
5755
5687
|
|
|
5756
5688
|
// src/index.ts
|
|
5757
|
-
var VERSION = "0.1.
|
|
5689
|
+
var VERSION = "0.1.8";
|
|
5758
5690
|
var program = new Command20();
|
|
5759
5691
|
var cmdName = brand.commands[0];
|
|
5760
5692
|
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({
|