@conceptcraft/mindframes 0.1.6 → 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 +1487 -1550
- 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
|
);
|
|
@@ -1025,7 +1084,7 @@ async function pollForCompletion(checkFn, maxAttempts = 60, intervalMs = 2e3) {
|
|
|
1025
1084
|
if (result.status === "completed" || result.status === "failed") {
|
|
1026
1085
|
return result;
|
|
1027
1086
|
}
|
|
1028
|
-
await new Promise((
|
|
1087
|
+
await new Promise((resolve6) => setTimeout(resolve6, intervalMs));
|
|
1029
1088
|
}
|
|
1030
1089
|
throw new ApiError("Operation timed out", 408, 1);
|
|
1031
1090
|
}
|
|
@@ -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);
|
|
@@ -1132,10 +1192,10 @@ function generateState() {
|
|
|
1132
1192
|
async function findAvailablePort(start, end) {
|
|
1133
1193
|
for (let port = start; port <= end; port++) {
|
|
1134
1194
|
try {
|
|
1135
|
-
await new Promise((
|
|
1195
|
+
await new Promise((resolve6, reject) => {
|
|
1136
1196
|
const server = http.createServer();
|
|
1137
1197
|
server.listen(port, () => {
|
|
1138
|
-
server.close(() =>
|
|
1198
|
+
server.close(() => resolve6());
|
|
1139
1199
|
});
|
|
1140
1200
|
server.on("error", reject);
|
|
1141
1201
|
});
|
|
@@ -1193,7 +1253,7 @@ async function exchangeCodeForTokens(tokenEndpoint, code, codeVerifier, redirect
|
|
|
1193
1253
|
return response.json();
|
|
1194
1254
|
}
|
|
1195
1255
|
function startCallbackServer(port, expectedState) {
|
|
1196
|
-
return new Promise((
|
|
1256
|
+
return new Promise((resolve6, reject) => {
|
|
1197
1257
|
let timeoutId;
|
|
1198
1258
|
let settled = false;
|
|
1199
1259
|
const cleanup = () => {
|
|
@@ -1279,7 +1339,7 @@ function startCallbackServer(port, expectedState) {
|
|
|
1279
1339
|
</html>
|
|
1280
1340
|
`);
|
|
1281
1341
|
cleanup();
|
|
1282
|
-
|
|
1342
|
+
resolve6({ code, state });
|
|
1283
1343
|
});
|
|
1284
1344
|
server.listen(port);
|
|
1285
1345
|
process.once("SIGINT", onCancel);
|
|
@@ -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();
|
|
@@ -2422,21 +2433,21 @@ Uploading ${options.file.length} file(s)...`));
|
|
|
2422
2433
|
}
|
|
2423
2434
|
});
|
|
2424
2435
|
async function readStdin() {
|
|
2425
|
-
return new Promise((
|
|
2436
|
+
return new Promise((resolve6) => {
|
|
2426
2437
|
let data = "";
|
|
2427
2438
|
process.stdin.setEncoding("utf8");
|
|
2428
2439
|
if (process.stdin.isTTY) {
|
|
2429
|
-
|
|
2440
|
+
resolve6("");
|
|
2430
2441
|
return;
|
|
2431
2442
|
}
|
|
2432
2443
|
process.stdin.on("data", (chunk) => {
|
|
2433
2444
|
data += chunk;
|
|
2434
2445
|
});
|
|
2435
2446
|
process.stdin.on("end", () => {
|
|
2436
|
-
|
|
2447
|
+
resolve6(data.trim());
|
|
2437
2448
|
});
|
|
2438
2449
|
setTimeout(() => {
|
|
2439
|
-
|
|
2450
|
+
resolve6(data.trim());
|
|
2440
2451
|
}, 100);
|
|
2441
2452
|
});
|
|
2442
2453
|
}
|
|
@@ -3127,1609 +3138,1188 @@ var whoamiCommand = new Command13("whoami").description("Show current user and t
|
|
|
3127
3138
|
}
|
|
3128
3139
|
});
|
|
3129
3140
|
|
|
3130
|
-
// src/commands/skill.ts
|
|
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
|
-
import { mkdirSync, writeFileSync, existsSync as existsSync2 } from "fs";
|
|
3135
|
-
import { join } from "path";
|
|
3136
|
-
import { homedir } from "os";
|
|
3137
|
-
import { execSync } from "child_process";
|
|
3138
|
-
function generateSkillContent(b) {
|
|
3139
|
-
const cmd2 = b.name;
|
|
3140
|
-
const pkg = b.packageName;
|
|
3141
|
-
const url = b.apiUrl;
|
|
3142
|
-
const name = b.displayName;
|
|
3143
|
-
return `---
|
|
3144
|
-
name: ${cmd2}
|
|
3145
|
-
description: Create AI-powered presentations and videos. Use for slides, decks, video content, voiceovers, and music generation.
|
|
3146
|
-
metadata:
|
|
3147
|
-
tags: presentations, video, tts, music, animation, remotion
|
|
3148
|
-
video:
|
|
3149
|
-
description: End-to-end AI video creation workflow with TTS voiceover and music generation. Use this skill when users want to create videos, promotional content, explainers, tourism videos, product demos, or any video content from an idea or topic. Handles the complete workflow - research, script writing, asset gathering, audio generation (voiceover + music), and orchestrates video creation. Use together with remotion-best-practices skill for Remotion-specific patterns. Triggers on requests like "create a video about X", "make a promotional video", "build a video for Y", or any video content creation task.
|
|
3150
|
-
---
|
|
3151
|
-
|
|
3152
|
-
# ${name} CLI
|
|
3153
|
-
|
|
3154
|
-
Create professional presentations and videos directly from your terminal.
|
|
3155
|
-
|
|
3156
|
-
## Prerequisites
|
|
3157
3146
|
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
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
|
|
3163
3152
|
|
|
3164
|
-
|
|
3153
|
+
### Related Skills
|
|
3165
3154
|
|
|
3166
|
-
|
|
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)
|
|
3167
3158
|
|
|
3168
|
-
|
|
3169
|
-
- [rules/video.md](rules/video.md) - Video creation workflow and commands
|
|
3170
|
-
- [rules/motion-standards.md](rules/motion-standards.md) - Animation quality standards
|
|
3171
|
-
- [rules/micro-interactions.md](rules/micro-interactions.md) - Animation components and patterns
|
|
3172
|
-
- **remotion-best-practices** skill (auto-installed) - Remotion-specific patterns
|
|
3159
|
+
---
|
|
3173
3160
|
|
|
3174
|
-
##
|
|
3161
|
+
## Core Rules
|
|
3175
3162
|
|
|
3176
|
-
|
|
3163
|
+
Your task is not "making slideshows" \u2014 you are **simulating a real interface** that obeys cinematic physics.
|
|
3177
3164
|
|
|
3178
|
-
|
|
3179
|
-
# Create from context
|
|
3180
|
-
cat README.md | ${cmd2} create "Project Overview"
|
|
3165
|
+
### Hard Constraints
|
|
3181
3166
|
|
|
3182
|
-
|
|
3183
|
-
|
|
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
|
|
3184
3173
|
|
|
3185
|
-
|
|
3186
|
-
${cmd2} create "API Docs" --slides 8 --tone educational --goal inform
|
|
3187
|
-
\`\`\`
|
|
3174
|
+
---
|
|
3188
3175
|
|
|
3189
|
-
|
|
3176
|
+
## Code Organization
|
|
3190
3177
|
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3178
|
+
- Create separate files: \`Button.tsx\`, \`Window.tsx\`, \`Cursor.tsx\`
|
|
3179
|
+
- Use Zod schemas for props validation
|
|
3180
|
+
- Extract animation configs to constants
|
|
3194
3181
|
|
|
3195
|
-
|
|
3196
|
-
|
|
3182
|
+
\`\`\`tsx
|
|
3183
|
+
import { spring, interpolate, useCurrentFrame, useVideoConfig } from 'remotion';
|
|
3197
3184
|
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
${cmd2} video search "tech workspace" -n 5
|
|
3185
|
+
// ALWAYS use spring for element entrances
|
|
3186
|
+
// NEVER use magic numbers
|
|
3201
3187
|
\`\`\`
|
|
3202
3188
|
|
|
3203
|
-
|
|
3189
|
+
---
|
|
3204
3190
|
|
|
3205
|
-
|
|
3206
|
-
# Text-to-speech
|
|
3207
|
-
${cmd2} tts generate -t "Narration text" -o voice.wav --voice Kore
|
|
3191
|
+
## Aesthetics (Linear/Stripe Style)
|
|
3208
3192
|
|
|
3209
|
-
|
|
3210
|
-
|
|
3193
|
+
\`\`\`css
|
|
3194
|
+
/* Shadows - soft, expensive */
|
|
3195
|
+
box-shadow: 0 20px 50px -12px rgba(0,0,0,0.5);
|
|
3211
3196
|
|
|
3212
|
-
|
|
3213
|
-
|
|
3197
|
+
/* Borders - thin, barely visible */
|
|
3198
|
+
border: 1px solid rgba(255,255,255,0.1);
|
|
3214
3199
|
\`\`\`
|
|
3215
3200
|
|
|
3216
|
-
|
|
3201
|
+
- Fonts: Inter or SF Pro
|
|
3202
|
+
- Never pure \`#000000\` \u2014 use \`#050505\`
|
|
3203
|
+
- Never pure \`#FFFFFF\` \u2014 use \`#F0F0F0\`
|
|
3217
3204
|
|
|
3218
|
-
|
|
3205
|
+
---
|
|
3219
3206
|
|
|
3220
|
-
##
|
|
3207
|
+
## Self-Check Before Render
|
|
3221
3208
|
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
name: presentations
|
|
3230
|
-
description: Creating AI-powered presentations
|
|
3231
|
-
---
|
|
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)
|
|
3232
3216
|
|
|
3233
|
-
|
|
3217
|
+
**If your video looks like PowerPoint with voiceover \u2014 START OVER.**
|
|
3218
|
+
`
|
|
3219
|
+
},
|
|
3220
|
+
{
|
|
3221
|
+
filename: "animation-physics.md",
|
|
3222
|
+
content: `# Animation Physics
|
|
3234
3223
|
|
|
3235
|
-
##
|
|
3224
|
+
## Spring Configurations
|
|
3236
3225
|
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3226
|
+
### Heavy UI (Modals, Sidebars)
|
|
3227
|
+
\`\`\`tsx
|
|
3228
|
+
config: { mass: 1, stiffness: 100, damping: 15 }
|
|
3229
|
+
\`\`\`
|
|
3240
3230
|
|
|
3241
|
-
|
|
3231
|
+
### Light UI (Tooltips, Badges)
|
|
3232
|
+
\`\`\`tsx
|
|
3233
|
+
config: { mass: 0.6, stiffness: 180, damping: 12 }
|
|
3234
|
+
\`\`\`
|
|
3242
3235
|
|
|
3243
|
-
|
|
3236
|
+
### Standard (Snappy)
|
|
3237
|
+
\`\`\`tsx
|
|
3238
|
+
config: { mass: 1, damping: 15, stiffness: 120 }
|
|
3239
|
+
\`\`\`
|
|
3244
3240
|
|
|
3245
|
-
|
|
3246
|
-
# Upload files (PDFs, PPTX, images, docs)
|
|
3247
|
-
${cmd2} create "Product Overview" --file ./deck.pptx --file ./logo.png
|
|
3241
|
+
---
|
|
3248
3242
|
|
|
3249
|
-
|
|
3250
|
-
${cmd2} create "Topic Title" --context "Key points, data, facts..."
|
|
3243
|
+
## Staggering
|
|
3251
3244
|
|
|
3252
|
-
|
|
3253
|
-
${cmd2} create "Topic Title" --context-file ./notes.md
|
|
3245
|
+
**NEVER show a list all at once.**
|
|
3254
3246
|
|
|
3255
|
-
|
|
3256
|
-
|
|
3247
|
+
\`\`\`tsx
|
|
3248
|
+
const STAGGER_FRAMES = 3; // 3-5 frames between items
|
|
3257
3249
|
|
|
3258
|
-
|
|
3259
|
-
|
|
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
|
+
});
|
|
3260
3257
|
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
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
|
+
})}
|
|
3265
3267
|
\`\`\`
|
|
3266
3268
|
|
|
3267
|
-
|
|
3269
|
+
---
|
|
3268
3270
|
|
|
3269
|
-
|
|
3270
|
-
|--------|-------------|---------|
|
|
3271
|
-
| \`-n, --slides <count>\` | Number of slides (1-20) | 10 |
|
|
3272
|
-
| \`-m, --mode <mode>\` | Quality: \`instant\`, \`fast\`, \`balanced\`, \`best\` | balanced |
|
|
3273
|
-
| \`-t, --tone <tone>\` | Tone: \`professional\`, \`educational\`, \`creative\`, \`formal\`, \`casual\` | professional |
|
|
3274
|
-
| \`--amount <amount>\` | Density: \`minimal\`, \`concise\`, \`detailed\`, \`extensive\` | concise |
|
|
3275
|
-
| \`--audience <text>\` | Target audience | General Audience |
|
|
3276
|
-
| \`-g, --goal <type>\` | Purpose: \`inform\`, \`persuade\`, \`train\`, \`learn\`, \`entertain\`, \`report\` | - |
|
|
3277
|
-
| \`-f, --file <paths...>\` | Files to upload | - |
|
|
3278
|
-
| \`-l, --language <lang>\` | Output language | en |
|
|
3279
|
-
| \`-b, --brand <id>\` | Branding ID | - |
|
|
3271
|
+
## Cursor Movement
|
|
3280
3272
|
|
|
3281
|
-
|
|
3273
|
+
**Cursor NEVER moves in straight lines.**
|
|
3282
3274
|
|
|
3283
|
-
\`\`\`
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
${cmd2} branding extract https://... # Extract branding from URL
|
|
3290
|
-
\`\`\`
|
|
3275
|
+
\`\`\`tsx
|
|
3276
|
+
const progress = spring({
|
|
3277
|
+
frame: frame - startFrame,
|
|
3278
|
+
fps,
|
|
3279
|
+
config: { damping: 20, stiffness: 80 },
|
|
3280
|
+
});
|
|
3291
3281
|
|
|
3292
|
-
|
|
3282
|
+
const linearX = interpolate(progress, [0, 1], [start.x, end.x]);
|
|
3283
|
+
const linearY = interpolate(progress, [0, 1], [start.y, end.y]);
|
|
3293
3284
|
|
|
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;
|
|
3294
3289
|
\`\`\`
|
|
3295
|
-
\u2713 Presentation created successfully
|
|
3296
3290
|
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3291
|
+
---
|
|
3292
|
+
|
|
3293
|
+
## Click Interaction
|
|
3300
3294
|
|
|
3301
|
-
|
|
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
|
|
3302
3301
|
\`\`\`
|
|
3303
|
-
|
|
3304
|
-
}
|
|
3305
|
-
function generateVideoRule(b) {
|
|
3306
|
-
const cmd2 = b.name;
|
|
3307
|
-
return `---
|
|
3308
|
-
name: video
|
|
3309
|
-
description: Video creation workflow - project-based UI replication AND stock-based videos
|
|
3302
|
+
|
|
3310
3303
|
---
|
|
3311
3304
|
|
|
3312
|
-
|
|
3305
|
+
## Timing Reference
|
|
3306
|
+
|
|
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
|
|
3313
3319
|
|
|
3314
|
-
|
|
3320
|
+
## SceneWrapper
|
|
3315
3321
|
|
|
3316
|
-
|
|
3322
|
+
\`\`\`tsx
|
|
3323
|
+
<SceneWrapper
|
|
3324
|
+
durationInFrames={300}
|
|
3325
|
+
transitionType="slideLeft"
|
|
3326
|
+
cameraMotion="panRight"
|
|
3327
|
+
>
|
|
3328
|
+
<FeatureLayer />
|
|
3329
|
+
<CursorLayer />
|
|
3330
|
+
<ParticleLayer />
|
|
3331
|
+
</SceneWrapper>
|
|
3332
|
+
\`\`\`
|
|
3317
3333
|
|
|
3318
3334
|
---
|
|
3319
3335
|
|
|
3320
|
-
##
|
|
3336
|
+
## Layer Structure (Z-Index)
|
|
3321
3337
|
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
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 |
|
|
3327
3347
|
|
|
3328
3348
|
---
|
|
3329
3349
|
|
|
3330
|
-
##
|
|
3331
|
-
|
|
3332
|
-
**Before doing ANYTHING, read these files:**
|
|
3350
|
+
## Case Study: SaaS Task Tracker
|
|
3333
3351
|
|
|
3334
|
-
1
|
|
3335
|
-
2. Read: rules/micro-interactions.md (animation patterns)
|
|
3336
|
-
3. Read: rules/component-integration.md (patterns)
|
|
3337
|
-
4. Read: rules/project-video-workflow.md (full workflow)
|
|
3338
|
-
5. Skill: remotion-best-practices
|
|
3352
|
+
### Scene 1: "The Hook" (~5s)
|
|
3339
3353
|
|
|
3340
|
-
|
|
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
|
|
3341
3357
|
|
|
3342
|
-
|
|
3358
|
+
### Scene 2: "Micro-Interaction" (~6s)
|
|
3343
3359
|
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
- **Result:** Video looks IDENTICAL to the real app
|
|
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
|
|
3349
3364
|
|
|
3350
|
-
###
|
|
3351
|
-
Use ONLY when user has NO project or explicitly wants stock content.
|
|
3352
|
-
- **Triggers:** "create a video about tourism", "make a generic explainer"
|
|
3353
|
-
- **Approach:** Use \`${cmd2} video create\` with stock images
|
|
3354
|
-
- **Result:** Generic video with stock imagery
|
|
3365
|
+
### Scene 3: "The Connection" (~5s)
|
|
3355
3366
|
|
|
3356
|
-
|
|
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
|
|
3357
3370
|
|
|
3358
3371
|
---
|
|
3359
3372
|
|
|
3360
|
-
##
|
|
3361
|
-
|
|
3362
|
-
Before creating a video, use \`AskUserQuestion\` tool (if available) to ask:
|
|
3373
|
+
## Composition
|
|
3363
3374
|
|
|
3364
|
-
\`\`\`
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
{
|
|
3379
|
-
"question": "What background music style fits your video?",
|
|
3380
|
-
"header": "Music",
|
|
3381
|
-
"options": [
|
|
3382
|
-
{ "label": "Uplifting/positive", "description": "Energetic and inspiring" },
|
|
3383
|
-
{ "label": "Corporate/professional", "description": "Modern, polished business feel" },
|
|
3384
|
-
{ "label": "Cinematic/dramatic", "description": "Epic, impactful presentation" },
|
|
3385
|
-
{ "label": "Calm ambient", "description": "Soft, subtle background" }
|
|
3386
|
-
],
|
|
3387
|
-
"multiSelect": false
|
|
3388
|
-
},
|
|
3389
|
-
{
|
|
3390
|
-
"question": "How long should the video be?",
|
|
3391
|
-
"header": "Duration",
|
|
3392
|
-
"options": [
|
|
3393
|
-
{ "label": "15 seconds", "description": "Quick teaser" },
|
|
3394
|
-
{ "label": "30 seconds", "description": "Social media friendly" },
|
|
3395
|
-
{ "label": "60 seconds", "description": "Standard length" }
|
|
3396
|
-
],
|
|
3397
|
-
"multiSelect": false
|
|
3398
|
-
}
|
|
3399
|
-
]
|
|
3400
|
-
}
|
|
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>
|
|
3401
3389
|
\`\`\`
|
|
3390
|
+
`
|
|
3391
|
+
},
|
|
3392
|
+
{
|
|
3393
|
+
filename: "scene-transitions.md",
|
|
3394
|
+
content: `# Scene Transitions
|
|
3402
3395
|
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
## Audio-First Workflow
|
|
3406
|
-
|
|
3407
|
-
**IMPORTANT:** This workflow ensures video and audio are always in sync. The CLI generates audio first, parses the script into sections, and calculates exact timing for each section. Scenes MUST use these timings.
|
|
3408
|
-
|
|
3409
|
-
### Step 1: Write Script
|
|
3396
|
+
**No FadeIn/FadeOut.** Only contextual transitions.
|
|
3410
3397
|
|
|
3411
|
-
|
|
3398
|
+
---
|
|
3412
3399
|
|
|
3413
|
-
|
|
3400
|
+
## Types
|
|
3414
3401
|
|
|
3415
|
-
###
|
|
3402
|
+
### 1. Object Persistence
|
|
3403
|
+
Same chart transforms (data, color, scale) while UI changes around it.
|
|
3416
3404
|
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
--script "Your narration script..." \\
|
|
3420
|
-
--topic "topic for image search" \\
|
|
3421
|
-
--voice Kore \\
|
|
3422
|
-
--music-prompt "uplifting corporate" \\
|
|
3423
|
-
--num-images 5 \\
|
|
3424
|
-
--output ./public
|
|
3425
|
-
\`\`\`
|
|
3405
|
+
### 2. Mask Reveal
|
|
3406
|
+
Button expands to screen size via SVG \`clipPath\`.
|
|
3426
3407
|
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
- \`public/audio/music.mp3\` - Background music (auto-matches voiceover length)
|
|
3430
|
-
- \`public/images/scene-*.jpg\` - Stock images
|
|
3431
|
-
- \`public/video-manifest.json\` - **Contains sections with exact TTS timestamps**
|
|
3408
|
+
### 3. Speed Ramps
|
|
3409
|
+
Scene A accelerates out, Scene B starts fast then slows.
|
|
3432
3410
|
|
|
3433
|
-
|
|
3411
|
+
---
|
|
3434
3412
|
|
|
3435
|
-
|
|
3413
|
+
## Match Cut Example
|
|
3436
3414
|
|
|
3437
|
-
\`\`\`
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
"characters": ["P", "u", "e", "r", "t", "o", " ", ...],
|
|
3444
|
-
"characterStartTimesSeconds": [0, 0.05, 0.1, ...],
|
|
3445
|
-
"characterEndTimesSeconds": [0.05, 0.1, 0.15, ...]
|
|
3446
|
-
}
|
|
3447
|
-
},
|
|
3448
|
-
"sections": [
|
|
3449
|
-
{
|
|
3450
|
-
"id": 1,
|
|
3451
|
-
"text": "Puerto Rico. La Isla del Encanto.",
|
|
3452
|
-
"wordCount": 5,
|
|
3453
|
-
"startTime": 0,
|
|
3454
|
-
"endTime": 2.8,
|
|
3455
|
-
"durationInSeconds": 2.8,
|
|
3456
|
-
"durationInFrames": 84,
|
|
3457
|
-
"imagePath": "images/scene-1.jpg"
|
|
3458
|
-
},
|
|
3459
|
-
{
|
|
3460
|
-
"id": 2,
|
|
3461
|
-
"text": "Discover five hundred years of history.",
|
|
3462
|
-
"wordCount": 7,
|
|
3463
|
-
"startTime": 2.8,
|
|
3464
|
-
"endTime": 8.2,
|
|
3465
|
-
"durationInSeconds": 5.4,
|
|
3466
|
-
"durationInFrames": 162,
|
|
3467
|
-
"imagePath": "images/scene-2.jpg"
|
|
3468
|
-
}
|
|
3469
|
-
],
|
|
3470
|
-
"totalDurationInFrames": 450,
|
|
3471
|
-
"fps": 30
|
|
3472
|
-
}
|
|
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
|
|
3473
3421
|
\`\`\`
|
|
3474
3422
|
|
|
3475
|
-
|
|
3476
|
-
- Section timing is derived from actual TTS audio timestamps (not estimated)
|
|
3477
|
-
- \`voiceover.timestamps\` contains character-level timing for word-by-word animations
|
|
3478
|
-
- Video duration will always match voiceover duration exactly
|
|
3479
|
-
|
|
3480
|
-
### Step 4: Create Scenes (Match Section Timing)
|
|
3423
|
+
---
|
|
3481
3424
|
|
|
3482
|
-
|
|
3425
|
+
## Overlapping Sequences (CRITICAL)
|
|
3483
3426
|
|
|
3484
3427
|
\`\`\`tsx
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
// ... etc
|
|
3492
|
-
|
|
3493
|
-
export const FULL_VIDEO_DURATION = manifest.totalDurationInFrames; // 450
|
|
3428
|
+
<Sequence from={0} durationInFrames={100}>
|
|
3429
|
+
<SceneOne />
|
|
3430
|
+
</Sequence>
|
|
3431
|
+
<Sequence from={85} durationInFrames={150}> {/* 15 frames early! */}
|
|
3432
|
+
<SceneTwo />
|
|
3433
|
+
</Sequence>
|
|
3494
3434
|
\`\`\`
|
|
3495
3435
|
|
|
3496
|
-
|
|
3436
|
+
---
|
|
3437
|
+
|
|
3438
|
+
## TransitionSeries
|
|
3497
3439
|
|
|
3498
3440
|
\`\`\`tsx
|
|
3499
|
-
|
|
3500
|
-
import {
|
|
3501
|
-
|
|
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
|
|
3502
3462
|
|
|
3503
|
-
|
|
3504
|
-
export const SCENE_1_DURATION = section.durationInFrames;
|
|
3463
|
+
## Reflection (Glass Glint)
|
|
3505
3464
|
|
|
3506
|
-
|
|
3507
|
-
const frame = useCurrentFrame();
|
|
3508
|
-
const { fps } = useVideoConfig();
|
|
3509
|
-
const progress = spring({ frame, fps, config: { damping: 15, stiffness: 100 } });
|
|
3465
|
+
Diagonal gradient sweeps every 5 seconds.
|
|
3510
3466
|
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
bottom: 100,
|
|
3517
|
-
left: 0,
|
|
3518
|
-
right: 0,
|
|
3519
|
-
textAlign: 'center',
|
|
3520
|
-
opacity: progress,
|
|
3521
|
-
transform: \`translateY(\${(1 - progress) * 20}px)\`,
|
|
3522
|
-
}}>
|
|
3523
|
-
<h1 style={{ color: 'white', fontSize: 60, textShadow: '2px 2px 8px rgba(0,0,0,0.8)' }}>
|
|
3524
|
-
{section.text}
|
|
3525
|
-
</h1>
|
|
3526
|
-
</div>
|
|
3527
|
-
</AbsoluteFill>
|
|
3528
|
-
);
|
|
3529
|
-
};
|
|
3467
|
+
\`\`\`tsx
|
|
3468
|
+
const cycleFrame = frame % 300;
|
|
3469
|
+
const sweepProgress = interpolate(cycleFrame, [0, 60], [-100, 200], {
|
|
3470
|
+
extrapolateRight: 'clamp',
|
|
3471
|
+
});
|
|
3530
3472
|
\`\`\`
|
|
3531
3473
|
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
\`\`\`tsx
|
|
3535
|
-
import { AbsoluteFill, Series, Audio, staticFile, useCurrentFrame, interpolate } from "remotion";
|
|
3536
|
-
import manifest from '../../public/video-manifest.json';
|
|
3537
|
-
import { Scene1, SCENE_1_DURATION } from "./scenes/Scene1";
|
|
3538
|
-
import { Scene2, SCENE_2_DURATION } from "./scenes/Scene2";
|
|
3539
|
-
// ... import all scenes
|
|
3474
|
+
---
|
|
3540
3475
|
|
|
3541
|
-
|
|
3476
|
+
## Background Breathing
|
|
3542
3477
|
|
|
3543
|
-
|
|
3544
|
-
const frame = useCurrentFrame();
|
|
3545
|
-
const fadeIn = interpolate(frame, [0, 10], [0, 1], { extrapolateRight: "clamp" });
|
|
3546
|
-
const fadeOut = interpolate(frame, [FULL_VIDEO_DURATION - 20, FULL_VIDEO_DURATION], [1, 0], { extrapolateLeft: "clamp" });
|
|
3547
|
-
return <Audio src={staticFile("audio/music.mp3")} volume={fadeIn * fadeOut * 0.25} />;
|
|
3548
|
-
};
|
|
3478
|
+
Background is NEVER static.
|
|
3549
3479
|
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
<Series>
|
|
3554
|
-
<Series.Sequence durationInFrames={SCENE_1_DURATION}>
|
|
3555
|
-
<Scene1 />
|
|
3556
|
-
</Series.Sequence>
|
|
3557
|
-
<Series.Sequence durationInFrames={SCENE_2_DURATION}>
|
|
3558
|
-
<Scene2 />
|
|
3559
|
-
</Series.Sequence>
|
|
3560
|
-
{/* Add all sections */}
|
|
3561
|
-
</Series>
|
|
3562
|
-
|
|
3563
|
-
<Audio src={staticFile("audio/voiceover.wav")} volume={1} />
|
|
3564
|
-
<BackgroundMusic />
|
|
3565
|
-
</AbsoluteFill>
|
|
3566
|
-
);
|
|
3567
|
-
};
|
|
3480
|
+
\`\`\`tsx
|
|
3481
|
+
const orb1X = Math.sin(frame / 60) * 200;
|
|
3482
|
+
const orb1Y = Math.cos(frame / 80) * 100;
|
|
3568
3483
|
\`\`\`
|
|
3569
3484
|
|
|
3570
|
-
|
|
3485
|
+
---
|
|
3571
3486
|
|
|
3572
|
-
|
|
3573
|
-
npm run dev # Preview in Remotion Studio
|
|
3574
|
-
npm run render # Output to out/video.mp4
|
|
3575
|
-
\`\`\`
|
|
3487
|
+
## Typewriter Effect
|
|
3576
3488
|
|
|
3577
|
-
|
|
3489
|
+
\`\`\`tsx
|
|
3490
|
+
const charIndex = Math.floor(frame / 3);
|
|
3491
|
+
const showCursor = Math.floor(frame / 15) % 2 === 0;
|
|
3578
3492
|
|
|
3579
|
-
|
|
3493
|
+
<span>
|
|
3494
|
+
{text.slice(0, charIndex)}
|
|
3495
|
+
{showCursor && <span>|</span>}
|
|
3496
|
+
</span>
|
|
3497
|
+
\`\`\`
|
|
3580
3498
|
|
|
3581
|
-
|
|
3582
|
-
|--------|----------|---------|-------------|
|
|
3583
|
-
| \`-s, --script <text>\` | Yes* | - | Narration script |
|
|
3584
|
-
| \`--script-file <path>\` | Yes* | - | Path to script file |
|
|
3585
|
-
| \`-t, --topic <text>\` | No | auto | Topic for image search |
|
|
3586
|
-
| \`-v, --voice <name>\` | No | Kore | TTS voice |
|
|
3587
|
-
| \`-m, --music-prompt <text>\` | No | auto | Music description |
|
|
3588
|
-
| \`-n, --num-images <n>\` | No | 5 | Number of images |
|
|
3589
|
-
| \`-o, --output <dir>\` | No | ./public | Output directory |
|
|
3499
|
+
---
|
|
3590
3500
|
|
|
3591
|
-
|
|
3501
|
+
## Vignette & Noise
|
|
3592
3502
|
|
|
3593
|
-
\`\`\`
|
|
3594
|
-
|
|
3595
|
-
|
|
3503
|
+
\`\`\`tsx
|
|
3504
|
+
// Noise
|
|
3505
|
+
<AbsoluteFill style={{
|
|
3506
|
+
backgroundImage: 'url(/noise.png)',
|
|
3507
|
+
opacity: 0.03,
|
|
3508
|
+
mixBlendMode: 'overlay',
|
|
3509
|
+
}} />
|
|
3510
|
+
|
|
3511
|
+
// Vignette
|
|
3512
|
+
<AbsoluteFill style={{
|
|
3513
|
+
background: 'radial-gradient(ellipse at center, transparent 40%, rgba(0,0,0,0.8) 100%)',
|
|
3514
|
+
}} />
|
|
3596
3515
|
\`\`\`
|
|
3516
|
+
`
|
|
3517
|
+
},
|
|
3518
|
+
{
|
|
3519
|
+
filename: "advanced-techniques.md",
|
|
3520
|
+
content: `# Advanced Techniques
|
|
3597
3521
|
|
|
3598
|
-
|
|
3522
|
+
## Audio-Reactive
|
|
3599
3523
|
|
|
3600
|
-
|
|
3524
|
+
- **Kick:** \`scale(1.005)\` pulse
|
|
3525
|
+
- **Snare:** Trigger scene changes
|
|
3526
|
+
- **Hi-hats:** Cursor flicker, particle shimmer
|
|
3601
3527
|
|
|
3602
|
-
|
|
3603
|
-
${cmd2} music generate -p "uplifting corporate" -d 30 -o music.mp3
|
|
3604
|
-
\`\`\`
|
|
3528
|
+
---
|
|
3605
3529
|
|
|
3606
|
-
|
|
3530
|
+
## Motion Blur (Fake)
|
|
3607
3531
|
|
|
3608
|
-
|
|
3532
|
+
\`\`\`tsx
|
|
3533
|
+
// Trail: Render 3-4 copies with opacity 0.3, 1-frame delay
|
|
3609
3534
|
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
${cmd2} video search "tech workspace" -n 5
|
|
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))\`
|
|
3613
3537
|
\`\`\`
|
|
3614
3538
|
|
|
3615
|
-
|
|
3539
|
+
---
|
|
3616
3540
|
|
|
3617
|
-
|
|
3541
|
+
## 3D Perspective
|
|
3618
3542
|
|
|
3619
|
-
\`\`\`
|
|
3620
|
-
|
|
3543
|
+
\`\`\`tsx
|
|
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>
|
|
3621
3552
|
\`\`\`
|
|
3622
3553
|
|
|
3623
|
-
|
|
3554
|
+
---
|
|
3624
3555
|
|
|
3625
|
-
|
|
3626
|
-
|---------|--------|-------|
|
|
3627
|
-
| Voiceover | 100% | Primary audio |
|
|
3628
|
-
| Background music | 20-30% | Fade in/out over ~10-20 frames |
|
|
3556
|
+
## Kinetic Typography
|
|
3629
3557
|
|
|
3630
|
-
|
|
3558
|
+
### Masked Reveal
|
|
3559
|
+
\`\`\`tsx
|
|
3560
|
+
<div style={{ overflow: 'hidden', height: 80 }}>
|
|
3561
|
+
<h1 style={{
|
|
3562
|
+
transform: \`translateY(\${interpolate(progress, [0, 1], [100, 0])}%)\`,
|
|
3563
|
+
}}>
|
|
3564
|
+
INTRODUCING
|
|
3565
|
+
</h1>
|
|
3566
|
+
</div>
|
|
3567
|
+
\`\`\`
|
|
3631
3568
|
|
|
3632
|
-
|
|
3569
|
+
### Keyword Animation
|
|
3570
|
+
Animate keywords, not whole sentences.
|
|
3571
|
+
`
|
|
3572
|
+
},
|
|
3573
|
+
{
|
|
3574
|
+
filename: "remotion-config.md",
|
|
3575
|
+
content: `# Remotion Configuration
|
|
3633
3576
|
|
|
3634
|
-
|
|
3577
|
+
## FPS & Resolution
|
|
3635
3578
|
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
4. **Micro-interactions** - Use components from micro-interactions.md for buttons, text reveals, highlights
|
|
3579
|
+
- **60 FPS mandatory** \u2014 30fps looks choppy
|
|
3580
|
+
- **1920\xD71080** Full HD
|
|
3581
|
+
- **Center:** \`{x: 960, y: 540}\`
|
|
3640
3582
|
|
|
3641
3583
|
---
|
|
3642
3584
|
|
|
3643
|
-
|
|
3585
|
+
## Timing
|
|
3644
3586
|
|
|
3645
|
-
|
|
3587
|
+
- 1 second = 60 frames
|
|
3588
|
+
- Fast interaction = 15-20 frames
|
|
3589
|
+
- No scene > 8 seconds without action
|
|
3646
3590
|
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
### 1.1 Find Brand Assets
|
|
3650
|
-
|
|
3651
|
-
\`\`\`bash
|
|
3652
|
-
# Logo
|
|
3653
|
-
find src -name "*[Ll]ogo*" 2>/dev/null
|
|
3654
|
-
find public -name "*logo*" 2>/dev/null
|
|
3591
|
+
---
|
|
3655
3592
|
|
|
3656
|
-
|
|
3657
|
-
cat tailwind.config.* | grep -A 30 "colors"
|
|
3658
|
-
cat src/app/globals.css | head -50
|
|
3593
|
+
## Entry-Action-Exit Structure
|
|
3659
3594
|
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3595
|
+
| Phase | Duration |
|
|
3596
|
+
|-------|----------|
|
|
3597
|
+
| Entry | 0.0s - 0.5s |
|
|
3598
|
+
| Action | 0.5s - (duration - 1s) |
|
|
3599
|
+
| Exit | last 1s |
|
|
3663
3600
|
|
|
3664
|
-
|
|
3601
|
+
---
|
|
3665
3602
|
|
|
3666
|
-
|
|
3603
|
+
## Font Loading
|
|
3667
3604
|
|
|
3668
|
-
\`\`\`
|
|
3669
|
-
|
|
3670
|
-
find src/components -name "*.tsx" | head -30
|
|
3605
|
+
\`\`\`tsx
|
|
3606
|
+
const [handle] = useState(() => delayRender());
|
|
3671
3607
|
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3608
|
+
useEffect(() => {
|
|
3609
|
+
document.fonts.ready.then(() => {
|
|
3610
|
+
continueRender(handle);
|
|
3611
|
+
});
|
|
3612
|
+
}, [handle]);
|
|
3676
3613
|
\`\`\`
|
|
3677
3614
|
|
|
3678
|
-
|
|
3679
|
-
- Layout structure (sidebar? grid? list?)
|
|
3680
|
-
- Colors used (bg-slate-900, text-teal-400, etc.)
|
|
3681
|
-
- Visual elements (badges, icons, thumbnails)
|
|
3682
|
-
- Typography (font sizes, weights)
|
|
3683
|
-
|
|
3684
|
-
### 1.3 Document Your Findings
|
|
3685
|
-
|
|
3686
|
-
\`\`\`markdown
|
|
3687
|
-
## Brand Discovery: [App Name]
|
|
3615
|
+
---
|
|
3688
3616
|
|
|
3689
|
-
|
|
3690
|
-
- Background: #0f172a (slate-900)
|
|
3691
|
-
- Surface: #1e293b (slate-800)
|
|
3692
|
-
- Primary: #14b8a6 (teal-500)
|
|
3693
|
-
- Accent: #f472b6 (pink-400)
|
|
3694
|
-
- Text: #ffffff / #94a3b8 (slate-400)
|
|
3617
|
+
## Zod Schema
|
|
3695
3618
|
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
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
|
+
});
|
|
3700
3625
|
\`\`\`
|
|
3701
3626
|
|
|
3702
3627
|
---
|
|
3703
3628
|
|
|
3704
|
-
##
|
|
3629
|
+
## SaaS Video Kit Components
|
|
3705
3630
|
|
|
3706
|
-
|
|
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 |
|
|
3707
3638
|
|
|
3708
|
-
|
|
3709
|
-
## Video Plan: [App Name] Demo
|
|
3710
|
-
|
|
3711
|
-
### Scene 1: Intro (3s / 90 frames)
|
|
3712
|
-
**What to show:** Logo + tagline on dark background
|
|
3713
|
-
**Colors:** bg #0f172a, logo centered
|
|
3714
|
-
**Animation:** Logo scales in with spring, tagline fades up
|
|
3639
|
+
---
|
|
3715
3640
|
|
|
3716
|
-
|
|
3717
|
-
**What to show:** Replicate the slides sidebar
|
|
3718
|
-
**Reference:** Read src/components/slides/SlidesSidebar.tsx
|
|
3719
|
-
**Build:** Dark sidebar with slide items, thumbnails
|
|
3720
|
-
**Animation:** Sidebar slides in, items stagger
|
|
3641
|
+
## Code Rules
|
|
3721
3642
|
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
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
|
|
3726
3651
|
|
|
3727
|
-
|
|
3728
|
-
**What to show:** Logo + CTA button + URL
|
|
3729
|
-
**Animation:** Logo fades in, button pulses
|
|
3730
|
-
\`\`\`
|
|
3652
|
+
For Stripe/Apple/Linear quality.
|
|
3731
3653
|
|
|
3732
3654
|
---
|
|
3733
3655
|
|
|
3734
|
-
##
|
|
3735
|
-
|
|
3736
|
-
### 3.1 Create Fresh Project
|
|
3656
|
+
## Global Lighting Engine
|
|
3737
3657
|
|
|
3738
|
-
\`\`\`
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3658
|
+
\`\`\`tsx
|
|
3659
|
+
const lightSource = { x: 0.2, y: -0.5 };
|
|
3660
|
+
const gradientAngle = Math.atan2(lightSource.y, lightSource.x) * (180 / Math.PI);
|
|
3661
|
+
|
|
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
|
+
}} />
|
|
3742
3667
|
\`\`\`
|
|
3743
3668
|
|
|
3744
|
-
|
|
3669
|
+
---
|
|
3745
3670
|
|
|
3746
|
-
|
|
3747
|
-
# Logo
|
|
3748
|
-
cp ../myapp/public/logo.svg ./public/
|
|
3671
|
+
## Noise & Dithering
|
|
3749
3672
|
|
|
3750
|
-
|
|
3751
|
-
cp ../myapp/tailwind.config.* ./
|
|
3673
|
+
Every background needs noise overlay (opacity 0.02-0.05). Prevents YouTube banding.
|
|
3752
3674
|
|
|
3753
|
-
|
|
3754
|
-
cp ../myapp/src/app/globals.css ./src/styles/
|
|
3755
|
-
\`\`\`
|
|
3675
|
+
---
|
|
3756
3676
|
|
|
3757
|
-
|
|
3677
|
+
## React Three Fiber
|
|
3758
3678
|
|
|
3759
|
-
|
|
3679
|
+
For particles, 3D globes \u2014 use WebGL via \`@remotion/three\`, not CSS 3D.
|
|
3760
3680
|
|
|
3761
3681
|
\`\`\`tsx
|
|
3762
|
-
|
|
3763
|
-
|
|
3682
|
+
import { ThreeCanvas } from '@remotion/three';
|
|
3683
|
+
|
|
3684
|
+
<AbsoluteFill>
|
|
3685
|
+
<HtmlUI />
|
|
3686
|
+
<ThreeCanvas>
|
|
3687
|
+
<Particles />
|
|
3688
|
+
</ThreeCanvas>
|
|
3689
|
+
</AbsoluteFill>
|
|
3690
|
+
\`\`\`
|
|
3764
3691
|
|
|
3765
|
-
|
|
3766
|
-
import { AbsoluteFill, useCurrentFrame, spring, useVideoConfig } from "remotion";
|
|
3692
|
+
See \`threejs-*\` skills for implementation.
|
|
3767
3693
|
|
|
3768
|
-
|
|
3769
|
-
{ id: 1, title: "Title Slide", selected: true },
|
|
3770
|
-
{ id: 2, title: "Overview", selected: false },
|
|
3771
|
-
{ id: 3, title: "Key Players", selected: false },
|
|
3772
|
-
];
|
|
3694
|
+
---
|
|
3773
3695
|
|
|
3774
|
-
|
|
3696
|
+
## Virtual Camera Rig
|
|
3775
3697
|
|
|
3776
|
-
|
|
3777
|
-
const frame = useCurrentFrame();
|
|
3778
|
-
const { fps } = useVideoConfig();
|
|
3698
|
+
Move camera, not elements:
|
|
3779
3699
|
|
|
3780
|
-
|
|
3781
|
-
|
|
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]);
|
|
3782
3705
|
|
|
3783
3706
|
return (
|
|
3784
|
-
<
|
|
3785
|
-
{
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
borderRight: "1px solid #1e293b",
|
|
3791
|
-
transform: \`translateX(\${sidebarX}px)\`,
|
|
3792
|
-
padding: 16,
|
|
3793
|
-
}}>
|
|
3794
|
-
{/* Header - EXACT styling from component */}
|
|
3795
|
-
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 16 }}>
|
|
3796
|
-
<span style={{ color: "#14b8a6", fontSize: 14, fontWeight: 500 }}>
|
|
3797
|
-
SLIDES CONTROL
|
|
3798
|
-
</span>
|
|
3799
|
-
</div>
|
|
3800
|
-
|
|
3801
|
-
{/* Slide items - staggered animation */}
|
|
3802
|
-
{mockSlides.map((slide, i) => {
|
|
3803
|
-
const itemProgress = spring({
|
|
3804
|
-
frame: frame - 10 - i * 8,
|
|
3805
|
-
fps,
|
|
3806
|
-
config: { damping: 15, stiffness: 100 },
|
|
3807
|
-
});
|
|
3808
|
-
|
|
3809
|
-
return (
|
|
3810
|
-
<div key={slide.id} style={{
|
|
3811
|
-
opacity: itemProgress,
|
|
3812
|
-
transform: \`translateX(\${(1 - itemProgress) * -20}px)\`,
|
|
3813
|
-
marginBottom: 8,
|
|
3814
|
-
padding: 12,
|
|
3815
|
-
borderRadius: 8,
|
|
3816
|
-
backgroundColor: slide.selected ? "#1e293b" : "transparent",
|
|
3817
|
-
border: slide.selected ? "1px solid #14b8a6" : "1px solid transparent",
|
|
3818
|
-
}}>
|
|
3819
|
-
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
|
|
3820
|
-
<div style={{ width: 48, height: 32, backgroundColor: "#334155", borderRadius: 4 }} />
|
|
3821
|
-
<div>
|
|
3822
|
-
<span style={{ color: "#64748b", fontSize: 12 }}>
|
|
3823
|
-
SLIDE {String(i + 1).padStart(2, "0")}
|
|
3824
|
-
</span>
|
|
3825
|
-
{slide.selected && <span style={{ color: "#f87171", fontSize: 12, marginLeft: 8 }}>SELECTED</span>}
|
|
3826
|
-
<p style={{ color: "#ffffff", fontSize: 14, margin: 0 }}>{slide.title}</p>
|
|
3827
|
-
</div>
|
|
3828
|
-
</div>
|
|
3829
|
-
</div>
|
|
3830
|
-
);
|
|
3831
|
-
})}
|
|
3832
|
-
</div>
|
|
3833
|
-
</AbsoluteFill>
|
|
3707
|
+
<div style={{
|
|
3708
|
+
transform: \`translateX(\${panX}px) scale(\${zoom})\`,
|
|
3709
|
+
transformOrigin: 'center',
|
|
3710
|
+
}}>
|
|
3711
|
+
{children}
|
|
3712
|
+
</div>
|
|
3834
3713
|
);
|
|
3835
3714
|
};
|
|
3836
3715
|
\`\`\`
|
|
3837
3716
|
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
**The video UI should be indistinguishable from the real app.**
|
|
3717
|
+
---
|
|
3841
3718
|
|
|
3842
|
-
|
|
3843
|
-
2. **EXACT spacing** - If \`p-4 gap-3\`, use \`padding: 16px, gap: 12px\`
|
|
3844
|
-
3. **EXACT typography** - Same font size, weight, color
|
|
3845
|
-
4. **EXACT borders** - Same border width, color, radius
|
|
3846
|
-
5. **EXACT layout** - Same flex direction, alignment, widths
|
|
3847
|
-
6. **Then add animations** - spring() entrances, stagger delays
|
|
3719
|
+
## Motion Rules
|
|
3848
3720
|
|
|
3849
|
-
|
|
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
|
|
3850
3728
|
|
|
3851
|
-
##
|
|
3729
|
+
## 1. Music Ends Before Video Finishes
|
|
3852
3730
|
|
|
3853
|
-
|
|
3731
|
+
**Problem:** Music duration is shorter than video duration, causing awkward silence at the end.
|
|
3854
3732
|
|
|
3855
|
-
|
|
3856
|
-
${cmd2} video create \\
|
|
3857
|
-
--script "Your narration..." \\
|
|
3858
|
-
--music-prompt "modern uplifting tech" \\
|
|
3859
|
-
--output ./public
|
|
3860
|
-
\`\`\`
|
|
3733
|
+
**Solution:** Loop music in Remotion using the \`loop\` prop:
|
|
3861
3734
|
|
|
3862
|
-
|
|
3735
|
+
\`\`\`tsx
|
|
3736
|
+
import { Audio } from 'remotion';
|
|
3863
3737
|
|
|
3864
|
-
|
|
3865
|
-
npm run dev # Preview
|
|
3866
|
-
npm run render # Output to out/video.mp4
|
|
3738
|
+
<Audio src={musicSrc} volume={0.3} loop />
|
|
3867
3739
|
\`\`\`
|
|
3868
3740
|
|
|
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
|
|
3745
|
+
|
|
3869
3746
|
---
|
|
3870
3747
|
|
|
3871
|
-
##
|
|
3748
|
+
## 2. Music Transitions Sound Abrupt
|
|
3872
3749
|
|
|
3873
|
-
|
|
3874
|
-
\`\`\`tsx
|
|
3875
|
-
// \u274C NO
|
|
3876
|
-
<div style={{ background: "linear-gradient(#667eea, #764ba2)", width: 200, height: 150 }} />
|
|
3877
|
-
\`\`\`
|
|
3750
|
+
**Problem:** Music cuts harshly when scenes change or video ends.
|
|
3878
3751
|
|
|
3879
|
-
|
|
3752
|
+
**Fix in Remotion:**
|
|
3880
3753
|
\`\`\`tsx
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3754
|
+
import { interpolate, Audio } from 'remotion';
|
|
3755
|
+
|
|
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
|
+
);
|
|
3885
3763
|
|
|
3886
|
-
|
|
3887
|
-
\`\`\`tsx
|
|
3888
|
-
// \u274C NO - App uses slate-900, not gray-800
|
|
3889
|
-
<div style={{ backgroundColor: "#1f2937" }}>
|
|
3764
|
+
<Audio src={music} volume={musicVolume} />
|
|
3890
3765
|
\`\`\`
|
|
3891
3766
|
|
|
3892
|
-
|
|
3767
|
+
---
|
|
3768
|
+
|
|
3769
|
+
## 3. Scene Transitions Too Harsh
|
|
3770
|
+
|
|
3771
|
+
**Problem:** Scenes change abruptly without smooth transitions.
|
|
3772
|
+
|
|
3773
|
+
**Fix:** Use \`@remotion/transitions\` with overlapping:
|
|
3893
3774
|
\`\`\`tsx
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
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>
|
|
3898
3791
|
\`\`\`
|
|
3899
3792
|
|
|
3900
3793
|
---
|
|
3901
3794
|
|
|
3902
|
-
##
|
|
3903
|
-
|
|
3904
|
-
### Before Building
|
|
3905
|
-
- [ ] Read motion-standards.md and micro-interactions.md
|
|
3906
|
-
- [ ] Found logo path
|
|
3907
|
-
- [ ] Found colors from tailwind.config
|
|
3908
|
-
- [ ] Read key components to understand visual structure
|
|
3909
|
-
- [ ] Documented findings
|
|
3910
|
-
- [ ] Planned scenes
|
|
3911
|
-
|
|
3912
|
-
### While Building
|
|
3913
|
-
- [ ] Using exact colors from tailwind.config
|
|
3914
|
-
- [ ] Matching layout structure of real app
|
|
3915
|
-
- [ ] Using spring() for animations
|
|
3916
|
-
- [ ] Mock data is realistic
|
|
3917
|
-
|
|
3918
|
-
### Before Render
|
|
3919
|
-
- [ ] Logo appears in intro and CTA
|
|
3920
|
-
- [ ] Colors match the app exactly
|
|
3921
|
-
- [ ] All scenes have smooth animations
|
|
3922
|
-
`;
|
|
3923
|
-
}
|
|
3924
|
-
function generateMotionStandardsRule() {
|
|
3925
|
-
return `---
|
|
3926
|
-
name: motion-standards
|
|
3927
|
-
description: Animation quality standards for high-end video production
|
|
3928
|
-
---
|
|
3929
|
-
|
|
3930
|
-
# Motion Design Standards
|
|
3795
|
+
## 4. Voiceover Lacks Energy
|
|
3931
3796
|
|
|
3932
|
-
|
|
3797
|
+
**Problem:** Voiceover sounds flat/monotone.
|
|
3933
3798
|
|
|
3934
|
-
**
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
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
|
+
}
|
|
3810
|
+
\`\`\`
|
|
3941
3811
|
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
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
|
|
3945
3815
|
|
|
3946
|
-
|
|
3947
|
-
const progress = spring({ frame, fps, config: { mass: 0.8, stiffness: 150, damping: 15 } });
|
|
3948
|
-
\`\`\`
|
|
3816
|
+
---
|
|
3949
3817
|
|
|
3950
|
-
##
|
|
3818
|
+
## 5. Video Duration Mismatch
|
|
3951
3819
|
|
|
3952
|
-
|
|
3953
|
-
- **Implementation:** Staggered entrances with 3-5 frames between items
|
|
3820
|
+
**Problem:** Brief says 30-45s but video is 20s (because scene duration = voiceover duration).
|
|
3954
3821
|
|
|
3822
|
+
**Fixes:**
|
|
3823
|
+
1. **Slow voice:** Use \`speed: 0.85\` in voiceSettings
|
|
3824
|
+
2. **Add padding in Remotion:** Hold last frame, add breathing room
|
|
3955
3825
|
\`\`\`tsx
|
|
3956
|
-
//
|
|
3957
|
-
|
|
3958
|
-
<FadeIn delay={8}><Content /></FadeIn>
|
|
3959
|
-
<FadeIn delay={16}><Footer /></FadeIn>
|
|
3960
|
-
|
|
3961
|
-
// GOOD - staggered list
|
|
3962
|
-
{items.map((item, i) => (
|
|
3963
|
-
<SlideUp key={item.id} delay={i * 4}>
|
|
3964
|
-
<ListItem data={item} />
|
|
3965
|
-
</SlideUp>
|
|
3966
|
-
))}
|
|
3826
|
+
// Add 30 frames (0.5s) padding after voiceover ends
|
|
3827
|
+
const paddedDuration = voiceoverFrames + 30;
|
|
3967
3828
|
\`\`\`
|
|
3829
|
+
3. **Brief should note:** "Duration based on voiceover length"
|
|
3968
3830
|
|
|
3969
|
-
|
|
3831
|
+
---
|
|
3970
3832
|
|
|
3971
|
-
|
|
3972
|
-
- **Implementation:** Dolly zoom (slow push in)
|
|
3833
|
+
## 6. Not Using Project UI Components
|
|
3973
3834
|
|
|
3974
|
-
|
|
3975
|
-
const CinematicContainer = ({ children }) => {
|
|
3976
|
-
const frame = useCurrentFrame();
|
|
3977
|
-
const { durationInFrames } = useVideoConfig();
|
|
3978
|
-
const scale = interpolate(frame, [0, durationInFrames], [1, 1.03]);
|
|
3835
|
+
**Problem:** Generic UI instead of pixel-perfect project components.
|
|
3979
3836
|
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
);
|
|
3985
|
-
};
|
|
3986
|
-
\`\`\`
|
|
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
|
|
3987
3841
|
|
|
3988
|
-
|
|
3842
|
+
\`\`\`tsx
|
|
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>
|
|
3854
|
+
\`\`\`
|
|
3989
3855
|
|
|
3990
|
-
|
|
3991
|
-
- **Implementation:** Use curved/Bezier paths for cursor movement
|
|
3856
|
+
---
|
|
3992
3857
|
|
|
3993
|
-
##
|
|
3858
|
+
## 7. Missing Physics & Lighting
|
|
3994
3859
|
|
|
3995
|
-
|
|
3996
|
-
2. **Layout:** Use \`AbsoluteFill\` for scene composition
|
|
3997
|
-
3. **State:** NO \`useState\` or \`useEffect\` - derive from \`useCurrentFrame()\`
|
|
3860
|
+
**Problem:** Video feels flat, no depth or motion.
|
|
3998
3861
|
|
|
3999
|
-
|
|
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
|
+
];
|
|
4000
3873
|
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
4. Add subtle camera movement
|
|
4005
|
-
5. Human touches for interactions
|
|
4006
|
-
`;
|
|
4007
|
-
}
|
|
4008
|
-
function generateMicroInteractionsRule() {
|
|
3874
|
+
// src/commands/skill/generate-video-skill.ts
|
|
3875
|
+
function generateVideoSkillContent(context) {
|
|
3876
|
+
const { name, cmd: cmd2, displayName } = context;
|
|
4009
3877
|
return `---
|
|
4010
|
-
name:
|
|
4011
|
-
description:
|
|
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.
|
|
4012
3880
|
---
|
|
4013
3881
|
|
|
4014
|
-
#
|
|
3882
|
+
# ${displayName} Video Creation CLI
|
|
4015
3883
|
|
|
4016
|
-
|
|
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.
|
|
4017
3885
|
|
|
4018
|
-
|
|
4019
|
-
2. **Purposeful** - Every animation communicates something
|
|
4020
|
-
3. **Physics-based** - Use \`spring()\`, not linear easing
|
|
4021
|
-
4. **Continuous** - Always have something moving subtly
|
|
3886
|
+
**Stack:** Remotion (React video framework) + React Three Fiber (R3F) + Three.js for 3D/WebGL, particles, shaders, lighting.
|
|
4022
3887
|
|
|
4023
|
-
|
|
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.
|
|
4024
3889
|
|
|
4025
|
-
|
|
4026
|
-
const SPRING_CONFIGS = {
|
|
4027
|
-
snappy: { damping: 15, stiffness: 200, mass: 0.5 },
|
|
4028
|
-
smooth: { damping: 20, stiffness: 100, mass: 1 },
|
|
4029
|
-
bouncy: { damping: 8, stiffness: 150, mass: 0.8 },
|
|
4030
|
-
gentle: { damping: 30, stiffness: 50, mass: 1 },
|
|
4031
|
-
};
|
|
4032
|
-
\`\`\`
|
|
3890
|
+
**Core Philosophy:** "Nothing sits still. Everything is physics-based. Every pixel breathes."
|
|
4033
3891
|
|
|
4034
|
-
|
|
3892
|
+
---
|
|
4035
3893
|
|
|
4036
|
-
|
|
3894
|
+
## CRITICAL: Professional Composition Rules
|
|
4037
3895
|
|
|
4038
|
-
|
|
4039
|
-
const AnimatedEntry = ({ delay = 0, direction = 'up', children }) => {
|
|
4040
|
-
const frame = useCurrentFrame();
|
|
4041
|
-
const { fps } = useVideoConfig();
|
|
3896
|
+
**These rules are MANDATORY for all marketing/product videos:**
|
|
4042
3897
|
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
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"
|
|
4048
3905
|
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
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
|
|
4055
3913
|
|
|
4056
|
-
|
|
3914
|
+
### Composition Examples:
|
|
4057
3915
|
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
};
|
|
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>
|
|
4067
3924
|
\`\`\`
|
|
4068
3925
|
|
|
4069
|
-
|
|
4070
|
-
|
|
3926
|
+
**\u2705 GOOD - Single Focus:**
|
|
4071
3927
|
\`\`\`tsx
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
</>
|
|
4078
|
-
);
|
|
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>
|
|
4079
3933
|
\`\`\`
|
|
4080
3934
|
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
### Button Press
|
|
4084
|
-
|
|
3935
|
+
**\u274C BAD - Flying Cards:**
|
|
4085
3936
|
\`\`\`tsx
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
const isPressing = frame >= pressFrame && frame < pressFrame + 3;
|
|
4091
|
-
const isReleasing = frame >= pressFrame + 3;
|
|
4092
|
-
|
|
4093
|
-
const releaseProgress = isReleasing ? spring({
|
|
4094
|
-
frame: frame - pressFrame - 3,
|
|
4095
|
-
fps,
|
|
4096
|
-
config: { damping: 10, stiffness: 300 }
|
|
4097
|
-
}) : 0;
|
|
4098
|
-
|
|
4099
|
-
const scale = isPressing ? 0.95 : (0.95 + releaseProgress * 0.05);
|
|
4100
|
-
|
|
4101
|
-
return <div style={{ transform: \`scale(\${scale})\` }}>{children}</div>;
|
|
4102
|
-
};
|
|
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
|
+
}} />
|
|
4103
3941
|
\`\`\`
|
|
4104
3942
|
|
|
4105
|
-
|
|
4106
|
-
|
|
3943
|
+
**\u2705 GOOD - Subtle Entrance:**
|
|
4107
3944
|
\`\`\`tsx
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
return (
|
|
4115
|
-
<span>
|
|
4116
|
-
{text.slice(0, Math.min(charsToShow, text.length))}
|
|
4117
|
-
{charsToShow < text.length && (
|
|
4118
|
-
<span style={{ opacity: frame % 15 < 8 ? 1 : 0 }}>|</span>
|
|
4119
|
-
)}
|
|
4120
|
-
</span>
|
|
4121
|
-
);
|
|
4122
|
-
};
|
|
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
|
+
}} />
|
|
4123
3951
|
\`\`\`
|
|
4124
3952
|
|
|
4125
|
-
###
|
|
3953
|
+
### Layout Grid System:
|
|
4126
3954
|
|
|
3955
|
+
**Use 12-column grid (like Bootstrap):**
|
|
4127
3956
|
\`\`\`tsx
|
|
4128
|
-
const
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
});
|
|
4133
|
-
const eased = 1 - Math.pow(1 - progress, 3);
|
|
4134
|
-
return <span>{Math.round(from + (to - from) * eased)}</span>;
|
|
3957
|
+
const GRID = {
|
|
3958
|
+
columns: 12,
|
|
3959
|
+
gutter: 40,
|
|
3960
|
+
padding: 120 // Edge padding
|
|
4135
3961
|
};
|
|
3962
|
+
|
|
3963
|
+
// Center 6 columns for main content
|
|
3964
|
+
const contentWidth = (1920 - (GRID.padding * 2) - (GRID.gutter * 5)) / 2;
|
|
4136
3965
|
\`\`\`
|
|
4137
3966
|
|
|
4138
|
-
|
|
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
|
|
4139
3972
|
|
|
4140
|
-
|
|
4141
|
-
|--------|----------|
|
|
4142
|
-
| Entry animation | 15-25 frames |
|
|
4143
|
-
| Button press | 10-15 frames |
|
|
4144
|
-
| Highlight/focus | 30-60 frames |
|
|
4145
|
-
| Stagger delay | 3-8 frames |
|
|
4146
|
-
`;
|
|
4147
|
-
}
|
|
4148
|
-
function generateAnimationComponents() {
|
|
4149
|
-
return `/**
|
|
4150
|
-
* Remotion Animation Components
|
|
4151
|
-
* Copy these into your project as needed.
|
|
4152
|
-
*/
|
|
4153
|
-
|
|
4154
|
-
import React from 'react';
|
|
4155
|
-
import { useCurrentFrame, useVideoConfig, interpolate, spring, Easing } from 'remotion';
|
|
4156
|
-
|
|
4157
|
-
// Spring configurations
|
|
4158
|
-
export const SPRING_CONFIGS = {
|
|
4159
|
-
snappy: { damping: 15, stiffness: 200, mass: 0.5 },
|
|
4160
|
-
smooth: { damping: 20, stiffness: 100, mass: 1 },
|
|
4161
|
-
bouncy: { damping: 8, stiffness: 150, mass: 0.8 },
|
|
4162
|
-
gentle: { damping: 30, stiffness: 50, mass: 1 },
|
|
4163
|
-
};
|
|
3973
|
+
---
|
|
4164
3974
|
|
|
4165
|
-
|
|
4166
|
-
export const AnimatedEntry: React.FC<{
|
|
4167
|
-
children: React.ReactNode;
|
|
4168
|
-
delay?: number;
|
|
4169
|
-
direction?: 'up' | 'down' | 'left' | 'right' | 'none';
|
|
4170
|
-
distance?: number;
|
|
4171
|
-
}> = ({ children, delay = 0, direction = 'up', distance = 30 }) => {
|
|
4172
|
-
const frame = useCurrentFrame();
|
|
4173
|
-
const { fps } = useVideoConfig();
|
|
3975
|
+
## Prerequisites
|
|
4174
3976
|
|
|
4175
|
-
|
|
4176
|
-
frame: frame - delay,
|
|
4177
|
-
fps,
|
|
4178
|
-
config: SPRING_CONFIGS.smooth,
|
|
4179
|
-
});
|
|
3977
|
+
Before using this skill, ensure you have:
|
|
4180
3978
|
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
none: { x: 0, y: 0 },
|
|
4187
|
-
};
|
|
3979
|
+
1. **Load related skills:**
|
|
3980
|
+
\`\`\`
|
|
3981
|
+
remotion-best-practices
|
|
3982
|
+
threejs-fundamentals
|
|
3983
|
+
\`\`\`
|
|
4188
3984
|
|
|
4189
|
-
|
|
3985
|
+
2. **Authenticate:**
|
|
3986
|
+
\`\`\`bash
|
|
3987
|
+
${cmd2} login
|
|
3988
|
+
\`\`\`
|
|
4190
3989
|
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
}}>
|
|
4196
|
-
{children}
|
|
4197
|
-
</div>
|
|
4198
|
-
);
|
|
4199
|
-
};
|
|
3990
|
+
3. **Remotion installed** (if creating videos):
|
|
3991
|
+
\`\`\`bash
|
|
3992
|
+
pnpm install remotion @remotion/cli
|
|
3993
|
+
\`\`\`
|
|
4200
3994
|
|
|
4201
|
-
|
|
4202
|
-
export const ScaleIn: React.FC<{
|
|
4203
|
-
children: React.ReactNode;
|
|
4204
|
-
delay?: number;
|
|
4205
|
-
from?: number;
|
|
4206
|
-
}> = ({ children, delay = 0, from = 0.8 }) => {
|
|
4207
|
-
const frame = useCurrentFrame();
|
|
4208
|
-
const { fps } = useVideoConfig();
|
|
3995
|
+
---
|
|
4209
3996
|
|
|
4210
|
-
|
|
4211
|
-
frame: frame - delay,
|
|
4212
|
-
fps,
|
|
4213
|
-
config: SPRING_CONFIGS.bouncy,
|
|
4214
|
-
});
|
|
3997
|
+
## Video Creation Workflow
|
|
4215
3998
|
|
|
4216
|
-
|
|
4217
|
-
<div style={{
|
|
4218
|
-
opacity: interpolate(progress, [0, 0.5], [0, 1], { extrapolateRight: 'clamp' }),
|
|
4219
|
-
transform: \`scale(\${interpolate(progress, [0, 1], [from, 1])})\`,
|
|
4220
|
-
}}>
|
|
4221
|
-
{children}
|
|
4222
|
-
</div>
|
|
4223
|
-
);
|
|
4224
|
-
};
|
|
3999
|
+
### Phase 0: Load Skills (MANDATORY)
|
|
4225
4000
|
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
}> = ({ children, itemDelay = 5, startFrame = 0 }) => (
|
|
4232
|
-
<>
|
|
4233
|
-
{React.Children.map(children, (child, i) => (
|
|
4234
|
-
<AnimatedEntry delay={startFrame + i * itemDelay}>{child}</AnimatedEntry>
|
|
4235
|
-
))}
|
|
4236
|
-
</>
|
|
4237
|
-
);
|
|
4001
|
+
Before ANY video work, invoke these skills:
|
|
4002
|
+
\`\`\`
|
|
4003
|
+
remotion-best-practices
|
|
4004
|
+
threejs-fundamentals
|
|
4005
|
+
\`\`\`
|
|
4238
4006
|
|
|
4239
|
-
|
|
4240
|
-
export const ButtonPress: React.FC<{
|
|
4241
|
-
children: React.ReactNode;
|
|
4242
|
-
pressFrame: number;
|
|
4243
|
-
}> = ({ children, pressFrame }) => {
|
|
4244
|
-
const frame = useCurrentFrame();
|
|
4245
|
-
const { fps } = useVideoConfig();
|
|
4007
|
+
### Phase 1: Discovery
|
|
4246
4008
|
|
|
4247
|
-
|
|
4248
|
-
|
|
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
|
|
4249
4013
|
|
|
4250
|
-
|
|
4251
|
-
frame: frame - pressFrame - 3,
|
|
4252
|
-
fps,
|
|
4253
|
-
config: { damping: 10, stiffness: 300 },
|
|
4254
|
-
}) : 0;
|
|
4014
|
+
### Phase 2: Video Brief
|
|
4255
4015
|
|
|
4256
|
-
|
|
4016
|
+
Present a brief outline (scenes \u22648s each, duration, assets found) and get user approval before production.
|
|
4257
4017
|
|
|
4258
|
-
|
|
4259
|
-
};
|
|
4018
|
+
### Phase 3: Production
|
|
4260
4019
|
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
showCursor?: boolean;
|
|
4267
|
-
}> = ({ text, startFrame = 0, speed = 2, showCursor = true }) => {
|
|
4268
|
-
const frame = useCurrentFrame();
|
|
4269
|
-
const charsToShow = Math.floor((frame - startFrame) / speed);
|
|
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
|
|
4270
4025
|
|
|
4271
|
-
|
|
4026
|
+
### Phase 4: Render & Thumbnail (REQUIRED)
|
|
4272
4027
|
|
|
4273
|
-
|
|
4028
|
+
\`\`\`bash
|
|
4029
|
+
# 1. Render the video (with voiceover and music already included)
|
|
4030
|
+
pnpm exec remotion render FullVideo
|
|
4274
4031
|
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
{showCursor && isTyping && (
|
|
4279
|
-
<span style={{ opacity: frame % 15 < 8 ? 1 : 0 }}>|</span>
|
|
4280
|
-
)}
|
|
4281
|
-
</span>
|
|
4282
|
-
);
|
|
4283
|
-
};
|
|
4032
|
+
# 2. ALWAYS embed thumbnail before delivering
|
|
4033
|
+
${cmd2} video thumbnail out/FullVideo.mp4 --frame 60
|
|
4034
|
+
\`\`\`
|
|
4284
4035
|
|
|
4285
|
-
|
|
4286
|
-
export const CountingNumber: React.FC<{
|
|
4287
|
-
from?: number;
|
|
4288
|
-
to: number;
|
|
4289
|
-
startFrame?: number;
|
|
4290
|
-
duration?: number;
|
|
4291
|
-
format?: (n: number) => string;
|
|
4292
|
-
}> = ({ from = 0, to, startFrame = 0, duration = 30, format = String }) => {
|
|
4293
|
-
const frame = useCurrentFrame();
|
|
4036
|
+
**Note:** Remotion videos include per-scene voiceovers and background music baked in during render.
|
|
4294
4037
|
|
|
4295
|
-
|
|
4296
|
-
extrapolateLeft: 'clamp',
|
|
4297
|
-
extrapolateRight: 'clamp',
|
|
4298
|
-
});
|
|
4038
|
+
---
|
|
4299
4039
|
|
|
4300
|
-
|
|
4301
|
-
const value = Math.round(from + (to - from) * eased);
|
|
4040
|
+
## Asset Generation
|
|
4302
4041
|
|
|
4303
|
-
|
|
4304
|
-
};
|
|
4042
|
+
Generate voiceover, music, and visual assets for each scene:
|
|
4305
4043
|
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4044
|
+
\`\`\`bash
|
|
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
|
|
4074
|
+
\`\`\`
|
|
4314
4075
|
|
|
4315
|
-
|
|
4316
|
-
|
|
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)
|
|
4317
4081
|
|
|
4318
|
-
|
|
4319
|
-
export const Highlight: React.FC<{
|
|
4320
|
-
children: React.ReactNode;
|
|
4321
|
-
startFrame: number;
|
|
4322
|
-
duration?: number;
|
|
4323
|
-
}> = ({ children, startFrame, duration = 45 }) => {
|
|
4324
|
-
const frame = useCurrentFrame();
|
|
4325
|
-
const { fps } = useVideoConfig();
|
|
4082
|
+
---
|
|
4326
4083
|
|
|
4327
|
-
|
|
4328
|
-
const progress = spring({
|
|
4329
|
-
frame: isActive ? frame - startFrame : 0,
|
|
4330
|
-
fps,
|
|
4331
|
-
config: SPRING_CONFIGS.snappy,
|
|
4332
|
-
});
|
|
4084
|
+
## Core Video Rules
|
|
4333
4085
|
|
|
4334
|
-
|
|
4086
|
+
${VIDEO_RULE_CONTENTS.map((rule) => rule.content).join("\n\n---\n\n")}
|
|
4335
4087
|
|
|
4336
|
-
|
|
4337
|
-
<div style={{
|
|
4338
|
-
transform: \`scale(\${scale})\`,
|
|
4339
|
-
boxShadow: isActive ? \`0 \${8 + progress * 12}px \${16 + progress * 24}px rgba(0,0,0,0.15)\` : undefined,
|
|
4340
|
-
}}>
|
|
4341
|
-
{children}
|
|
4342
|
-
</div>
|
|
4343
|
-
);
|
|
4344
|
-
};
|
|
4088
|
+
## Useful Commands
|
|
4345
4089
|
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
}> = ({ path, size = 24 }) => {
|
|
4351
|
-
const frame = useCurrentFrame();
|
|
4352
|
-
const { fps } = useVideoConfig();
|
|
4090
|
+
\`\`\`bash
|
|
4091
|
+
# Generate video assets
|
|
4092
|
+
${cmd2} video create < scenes.json
|
|
4093
|
+
cat scenes.json | ${cmd2} video create --output ./public
|
|
4353
4094
|
|
|
4354
|
-
|
|
4355
|
-
|
|
4095
|
+
# Initialize Remotion project
|
|
4096
|
+
${cmd2} video init my-video
|
|
4356
4097
|
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
const to = path[i + 1];
|
|
4098
|
+
# Embed thumbnail
|
|
4099
|
+
${cmd2} video thumbnail out/video.mp4 --frame 60
|
|
4360
4100
|
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
fps,
|
|
4365
|
-
config: { damping: 20, stiffness: 80 },
|
|
4366
|
-
});
|
|
4101
|
+
# Search for stock assets
|
|
4102
|
+
${cmd2} images search "mountain landscape" --limit 10
|
|
4103
|
+
${cmd2} videos search "ocean waves" --limit 5
|
|
4367
4104
|
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
x = to.x;
|
|
4373
|
-
y = to.y;
|
|
4374
|
-
}
|
|
4375
|
-
}
|
|
4105
|
+
# Generate audio
|
|
4106
|
+
${cmd2} audio generate "Your script here" --voice Kore
|
|
4107
|
+
${cmd2} music generate "upbeat corporate" --duration 30
|
|
4108
|
+
\`\`\`
|
|
4376
4109
|
|
|
4377
|
-
return (
|
|
4378
|
-
<div style={{
|
|
4379
|
-
position: 'absolute',
|
|
4380
|
-
left: \`\${x}%\`,
|
|
4381
|
-
top: \`\${y}%\`,
|
|
4382
|
-
transform: 'translate(-50%, -50%)',
|
|
4383
|
-
zIndex: 1000,
|
|
4384
|
-
pointerEvents: 'none',
|
|
4385
|
-
}}>
|
|
4386
|
-
<svg width={size} height={size} viewBox="0 0 24 24">
|
|
4387
|
-
<path
|
|
4388
|
-
d="M4 4 L4 20 L9 15 L13 22 L16 20 L12 13 L19 13 Z"
|
|
4389
|
-
fill="white"
|
|
4390
|
-
stroke="black"
|
|
4391
|
-
strokeWidth="1.5"
|
|
4392
|
-
/>
|
|
4393
|
-
</svg>
|
|
4394
|
-
</div>
|
|
4395
|
-
);
|
|
4396
|
-
};
|
|
4397
|
-
`;
|
|
4398
|
-
}
|
|
4399
|
-
function generateComponentIntegrationRule(b) {
|
|
4400
|
-
const cmd2 = b.name;
|
|
4401
|
-
return `---
|
|
4402
|
-
name: component-integration
|
|
4403
|
-
description: Integrating app components into Remotion videos
|
|
4404
4110
|
---
|
|
4405
4111
|
|
|
4406
|
-
|
|
4112
|
+
## Best Practices
|
|
4407
4113
|
|
|
4408
|
-
|
|
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
|
|
4409
4120
|
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
### Approach A: Replicate UI (Recommended)
|
|
4413
|
-
Read your app's components, note every visual detail, build identical-looking components in Remotion.
|
|
4121
|
+
---
|
|
4414
4122
|
|
|
4415
|
-
|
|
4123
|
+
## Troubleshooting
|
|
4416
4124
|
|
|
4417
|
-
|
|
4418
|
-
|
|
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
|
|
4419
4130
|
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
\`\`\`
|
|
4131
|
+
For detailed troubleshooting, see "Known Issues" section above.
|
|
4132
|
+
`;
|
|
4133
|
+
}
|
|
4424
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.
|
|
4425
4141
|
---
|
|
4426
4142
|
|
|
4427
|
-
|
|
4143
|
+
# ${displayName} Presentation CLI
|
|
4428
4144
|
|
|
4429
|
-
|
|
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.
|
|
4430
4146
|
|
|
4431
|
-
|
|
4432
|
-
// BEFORE (interactive app)
|
|
4433
|
-
<Button onClick={handleSubmit}>Submit</Button>
|
|
4434
|
-
|
|
4435
|
-
// AFTER (video-ready)
|
|
4436
|
-
<Button disabled style={{ pointerEvents: 'none' }}>Submit</Button>
|
|
4437
|
-
\`\`\`
|
|
4147
|
+
---
|
|
4438
4148
|
|
|
4439
|
-
|
|
4149
|
+
## Authentication
|
|
4440
4150
|
|
|
4441
|
-
\`\`\`
|
|
4442
|
-
|
|
4443
|
-
|
|
4151
|
+
\`\`\`bash
|
|
4152
|
+
# Login via OAuth
|
|
4153
|
+
${cmd2} login
|
|
4444
4154
|
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
{ id: 1, name: 'Sarah Chen', role: 'Designer' },
|
|
4448
|
-
{ id: 2, name: 'Alex Rivera', role: 'Developer' },
|
|
4449
|
-
];
|
|
4155
|
+
# Or set API key
|
|
4156
|
+
export ${name.toUpperCase().replace(/-/g, "_")}_API_KEY="your-key-here"
|
|
4450
4157
|
\`\`\`
|
|
4451
4158
|
|
|
4452
|
-
|
|
4159
|
+
---
|
|
4453
4160
|
|
|
4454
|
-
|
|
4455
|
-
import { FadeIn, SlideUp } from '../shared';
|
|
4161
|
+
## Creating Presentations
|
|
4456
4162
|
|
|
4457
|
-
|
|
4458
|
-
<Navbar />
|
|
4459
|
-
</FadeIn>
|
|
4163
|
+
### From Text
|
|
4460
4164
|
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
</SlideUp>
|
|
4165
|
+
\`\`\`bash
|
|
4166
|
+
${cmd2} create "AI-powered product analytics platform"
|
|
4464
4167
|
\`\`\`
|
|
4465
4168
|
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
## Common Showcase Patterns
|
|
4169
|
+
### From File
|
|
4469
4170
|
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
\`\`\`tsx
|
|
4473
|
-
const DashboardShowcase = () => {
|
|
4474
|
-
return (
|
|
4475
|
-
<DashboardLayout>
|
|
4476
|
-
<FadeIn delay={0}>
|
|
4477
|
-
<Header user={mockUser} />
|
|
4478
|
-
</FadeIn>
|
|
4479
|
-
|
|
4480
|
-
<div className="grid grid-cols-3 gap-4">
|
|
4481
|
-
<SlideUp delay={15}><StatsWidget data={revenueData} /></SlideUp>
|
|
4482
|
-
<SlideUp delay={23}><StatsWidget data={usersData} /></SlideUp>
|
|
4483
|
-
<SlideUp delay={31}><StatsWidget data={ordersData} /></SlideUp>
|
|
4484
|
-
</div>
|
|
4485
|
-
|
|
4486
|
-
<FadeIn delay={45}>
|
|
4487
|
-
<ChartWidget data={chartData} />
|
|
4488
|
-
</FadeIn>
|
|
4489
|
-
</DashboardLayout>
|
|
4490
|
-
);
|
|
4491
|
-
};
|
|
4171
|
+
\`\`\`bash
|
|
4172
|
+
${cmd2} create --file product-brief.md
|
|
4492
4173
|
\`\`\`
|
|
4493
4174
|
|
|
4494
|
-
###
|
|
4175
|
+
### From URL
|
|
4495
4176
|
|
|
4496
|
-
\`\`\`
|
|
4497
|
-
|
|
4498
|
-
const frame = useCurrentFrame();
|
|
4499
|
-
const { fps } = useVideoConfig();
|
|
4500
|
-
|
|
4501
|
-
return (
|
|
4502
|
-
<LoginForm>
|
|
4503
|
-
<Input
|
|
4504
|
-
label="Email"
|
|
4505
|
-
value={<TextReveal text="sarah@example.com" startFrame={0} />}
|
|
4506
|
-
/>
|
|
4507
|
-
<Input
|
|
4508
|
-
label="Password"
|
|
4509
|
-
type="password"
|
|
4510
|
-
value={frame > fps * 2 ? '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022' : ''}
|
|
4511
|
-
/>
|
|
4512
|
-
</LoginForm>
|
|
4513
|
-
);
|
|
4514
|
-
};
|
|
4177
|
+
\`\`\`bash
|
|
4178
|
+
${cmd2} create --url https://company.com/product
|
|
4515
4179
|
\`\`\`
|
|
4516
4180
|
|
|
4517
|
-
###
|
|
4518
|
-
|
|
4519
|
-
\`\`\`tsx
|
|
4520
|
-
const ModalShowcase = () => {
|
|
4521
|
-
const frame = useCurrentFrame();
|
|
4522
|
-
const showModal = frame > 30;
|
|
4181
|
+
### From Piped Content
|
|
4523
4182
|
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
{showModal && (
|
|
4528
|
-
<>
|
|
4529
|
-
<FadeIn delay={30}>
|
|
4530
|
-
<div className="absolute inset-0 bg-black/50" />
|
|
4531
|
-
</FadeIn>
|
|
4532
|
-
<SlideUp delay={35}>
|
|
4533
|
-
<ConfirmationModal title="Confirm Delete" message="Are you sure?" isOpen />
|
|
4534
|
-
</SlideUp>
|
|
4535
|
-
</>
|
|
4536
|
-
)}
|
|
4537
|
-
</>
|
|
4538
|
-
);
|
|
4539
|
-
};
|
|
4183
|
+
\`\`\`bash
|
|
4184
|
+
cat research.txt | ${cmd2} create
|
|
4185
|
+
pbpaste | ${cmd2} create
|
|
4540
4186
|
\`\`\`
|
|
4541
4187
|
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
## Troubleshooting
|
|
4188
|
+
### Advanced Options
|
|
4545
4189
|
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
</AuthContext.Provider>
|
|
4554
|
-
);
|
|
4190
|
+
\`\`\`bash
|
|
4191
|
+
${cmd2} create "Topic" \\
|
|
4192
|
+
--slides 10 \\
|
|
4193
|
+
--style professional \\
|
|
4194
|
+
--branding my-brand \\
|
|
4195
|
+
--template minimal \\
|
|
4196
|
+
--output presentation.zip
|
|
4555
4197
|
\`\`\`
|
|
4556
4198
|
|
|
4557
|
-
### Component too large for frame
|
|
4558
|
-
\`\`\`tsx
|
|
4559
|
-
// Use transform scale to fit
|
|
4560
|
-
<div style={{ transform: 'scale(0.8)', transformOrigin: 'top left' }}>
|
|
4561
|
-
<LargeComponent />
|
|
4562
|
-
</div>
|
|
4563
|
-
\`\`\`
|
|
4564
|
-
`;
|
|
4565
|
-
}
|
|
4566
|
-
function generateProjectVideoWorkflowRule(b) {
|
|
4567
|
-
const cmd2 = b.name;
|
|
4568
|
-
return `---
|
|
4569
|
-
name: project-video-workflow
|
|
4570
|
-
description: Create promotional videos using actual project UI
|
|
4571
4199
|
---
|
|
4572
4200
|
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
Create promotional videos using **your actual project's UI** replicated in Remotion.
|
|
4201
|
+
## Presentation Options
|
|
4576
4202
|
|
|
4577
|
-
|
|
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
|
|
4578
4209
|
|
|
4579
|
-
|
|
4580
|
-
- User wants "product demo", "feature walkthrough", or "promotional video"
|
|
4581
|
-
- User mentions showcasing specific features/UI
|
|
4582
|
-
- User wants to animate their actual app interface
|
|
4210
|
+
---
|
|
4583
4211
|
|
|
4584
|
-
##
|
|
4212
|
+
## Managing Presentations
|
|
4585
4213
|
|
|
4586
4214
|
\`\`\`bash
|
|
4587
|
-
#
|
|
4588
|
-
${cmd2}
|
|
4589
|
-
|
|
4215
|
+
# List all presentations
|
|
4216
|
+
${cmd2} list
|
|
4217
|
+
${cmd2} list --format json
|
|
4218
|
+
|
|
4219
|
+
# Get presentation details
|
|
4220
|
+
${cmd2} get <id-or-slug>
|
|
4590
4221
|
|
|
4591
|
-
#
|
|
4592
|
-
${cmd2}
|
|
4593
|
-
--script "Introducing our new app..." \\
|
|
4594
|
-
--output ./public
|
|
4222
|
+
# Export presentation
|
|
4223
|
+
${cmd2} export <id-or-slug> -o presentation.zip
|
|
4595
4224
|
|
|
4596
|
-
#
|
|
4225
|
+
# Import presentation
|
|
4226
|
+
${cmd2} import ./presentation.zip
|
|
4597
4227
|
|
|
4598
|
-
#
|
|
4599
|
-
|
|
4600
|
-
npm run render
|
|
4228
|
+
# Delete presentation
|
|
4229
|
+
${cmd2} delete <id-or-slug>
|
|
4601
4230
|
\`\`\`
|
|
4602
4231
|
|
|
4603
4232
|
---
|
|
4604
4233
|
|
|
4605
|
-
##
|
|
4606
|
-
|
|
4607
|
-
### Step 1: Analyze Project
|
|
4234
|
+
## Branding Management
|
|
4608
4235
|
|
|
4609
4236
|
\`\`\`bash
|
|
4610
|
-
#
|
|
4611
|
-
|
|
4237
|
+
# List saved brands
|
|
4238
|
+
${cmd2} branding list
|
|
4612
4239
|
|
|
4613
|
-
#
|
|
4614
|
-
|
|
4240
|
+
# Extract branding from website
|
|
4241
|
+
${cmd2} branding extract https://company.com
|
|
4615
4242
|
|
|
4616
|
-
#
|
|
4617
|
-
|
|
4243
|
+
# Use branding in presentation
|
|
4244
|
+
${cmd2} create "Topic" --branding company-brand
|
|
4618
4245
|
\`\`\`
|
|
4619
4246
|
|
|
4620
|
-
|
|
4621
|
-
- Framework: React, Next.js, Vue
|
|
4622
|
-
- Styling: Tailwind, CSS modules, styled-components
|
|
4623
|
-
- Key components: Forms, cards, modals, dashboards
|
|
4624
|
-
- Views to showcase
|
|
4625
|
-
|
|
4626
|
-
### Step 2: Document Brand
|
|
4247
|
+
---
|
|
4627
4248
|
|
|
4628
|
-
|
|
4629
|
-
## Brand: [App Name]
|
|
4249
|
+
## Stock Asset Search
|
|
4630
4250
|
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
- Text: #ffffff
|
|
4251
|
+
\`\`\`bash
|
|
4252
|
+
# Search for images
|
|
4253
|
+
${cmd2} images search "mountain landscape" --limit 10
|
|
4254
|
+
${cmd2} images search "business team" --format json
|
|
4636
4255
|
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
3. Modal - Overlay, card
|
|
4256
|
+
# Search for videos
|
|
4257
|
+
${cmd2} videos search "ocean waves" --limit 5
|
|
4258
|
+
${cmd2} videos search "city timelapse" --orientation landscape
|
|
4641
4259
|
\`\`\`
|
|
4642
4260
|
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
\`\`\`markdown
|
|
4646
|
-
## Scene Plan
|
|
4261
|
+
---
|
|
4647
4262
|
|
|
4648
|
-
|
|
4649
|
-
- Logo centered
|
|
4650
|
-
- Tagline fades up
|
|
4263
|
+
## Best Practices
|
|
4651
4264
|
|
|
4652
|
-
|
|
4653
|
-
-
|
|
4654
|
-
-
|
|
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
|
|
4655
4270
|
|
|
4656
|
-
|
|
4657
|
-
- Sidebar slides in
|
|
4658
|
-
- Selection animates
|
|
4271
|
+
---
|
|
4659
4272
|
|
|
4660
|
-
|
|
4661
|
-
- Logo + button
|
|
4662
|
-
\`\`\`
|
|
4273
|
+
## Troubleshooting
|
|
4663
4274
|
|
|
4664
|
-
|
|
4275
|
+
**Authentication Issues:**
|
|
4276
|
+
\`\`\`bash
|
|
4277
|
+
# Check current user
|
|
4278
|
+
${cmd2} whoami
|
|
4665
4279
|
|
|
4666
|
-
|
|
4280
|
+
# Re-authenticate
|
|
4281
|
+
${cmd2} logout
|
|
4282
|
+
${cmd2} login
|
|
4283
|
+
\`\`\`
|
|
4667
4284
|
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4285
|
+
**Generation Failures:**
|
|
4286
|
+
- Ensure input is clear and has enough context
|
|
4287
|
+
- Try different styles or templates
|
|
4288
|
+
- Check API status and quotas
|
|
4671
4289
|
|
|
4672
|
-
|
|
4290
|
+
**Export Issues:**
|
|
4291
|
+
- Verify output format is supported
|
|
4292
|
+
- Check file permissions in output directory
|
|
4293
|
+
- Ensure presentation ID is correct
|
|
4673
4294
|
|
|
4674
|
-
|
|
4675
|
-
revenue: 125000,
|
|
4676
|
-
users: 1234,
|
|
4677
|
-
orders: 567,
|
|
4678
|
-
};
|
|
4295
|
+
---
|
|
4679
4296
|
|
|
4680
|
-
|
|
4681
|
-
const frame = useCurrentFrame();
|
|
4682
|
-
const { fps } = useVideoConfig();
|
|
4297
|
+
## Examples
|
|
4683
4298
|
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
{/* Use EXACT colors from your tailwind.config */}
|
|
4688
|
-
</AbsoluteFill>
|
|
4689
|
-
);
|
|
4690
|
-
};
|
|
4299
|
+
**Quick pitch deck:**
|
|
4300
|
+
\`\`\`bash
|
|
4301
|
+
${cmd2} create "SaaS analytics platform for e-commerce" --slides 8 --style professional
|
|
4691
4302
|
\`\`\`
|
|
4692
4303
|
|
|
4693
|
-
|
|
4694
|
-
|
|
4304
|
+
**From product brief:**
|
|
4695
4305
|
\`\`\`bash
|
|
4696
|
-
${cmd2}
|
|
4697
|
-
--script "Introducing [App]. The fastest way to..." \\
|
|
4698
|
-
--music-prompt "modern uplifting tech" \\
|
|
4699
|
-
--output ./public
|
|
4306
|
+
${cmd2} create --file brief.md --branding acme --output pitch.zip
|
|
4700
4307
|
\`\`\`
|
|
4701
4308
|
|
|
4702
|
-
|
|
4703
|
-
|
|
4309
|
+
**Research presentation:**
|
|
4704
4310
|
\`\`\`bash
|
|
4705
|
-
|
|
4706
|
-
npm run render # Final video
|
|
4311
|
+
cat research-notes.txt | ${cmd2} create --slides 15 --style minimal
|
|
4707
4312
|
\`\`\`
|
|
4708
|
-
|
|
4709
|
-
---
|
|
4710
|
-
|
|
4711
|
-
## Tips
|
|
4712
|
-
|
|
4713
|
-
1. **Start simple** - Get basic scenes working before adding complex animations
|
|
4714
|
-
2. **Use mock data** - Pre-define realistic demo data
|
|
4715
|
-
3. **Match voiceover timing** - Sync visual transitions with narration
|
|
4716
|
-
4. **Keep scenes focused** - One main idea per scene
|
|
4717
|
-
5. **Test at 1x speed** - Preview at normal speed to catch timing issues
|
|
4718
4313
|
`;
|
|
4719
4314
|
}
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
"rules/project-video-workflow.md": generateProjectVideoWorkflowRule(b),
|
|
4729
|
-
"assets/animation-components.tsx": generateAnimationComponents()
|
|
4730
|
-
};
|
|
4731
|
-
}
|
|
4732
|
-
var EDITORS = [
|
|
4315
|
+
|
|
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";
|
|
4320
|
+
|
|
4321
|
+
// src/commands/skill/editors.ts
|
|
4322
|
+
var SUPPORTED_EDITORS = [
|
|
4733
4323
|
{ name: "Claude Code", dir: ".claude" },
|
|
4734
4324
|
{ name: "Cursor", dir: ".cursor" },
|
|
4735
4325
|
{ name: "Codex", dir: ".codex" },
|
|
@@ -4737,142 +4327,195 @@ var EDITORS = [
|
|
|
4737
4327
|
{ name: "Windsurf", dir: ".windsurf" },
|
|
4738
4328
|
{ name: "Agent", dir: ".agent" }
|
|
4739
4329
|
];
|
|
4740
|
-
var skillCommand = new Command14("skill").description(`Manage ${brand.displayName} skill for AI coding assistants`).addHelpText(
|
|
4741
|
-
"after",
|
|
4742
|
-
`
|
|
4743
|
-
${chalk12.bold("Examples:")}
|
|
4744
|
-
${chalk12.gray("# Install skill for all detected editors")}
|
|
4745
|
-
$ ${brand.name} skill install
|
|
4746
|
-
|
|
4747
|
-
${chalk12.gray("# Install to specific directory")}
|
|
4748
|
-
$ ${brand.name} skill install --dir ~/.claude
|
|
4749
|
-
|
|
4750
|
-
${chalk12.gray("# Install without remotion-best-practices")}
|
|
4751
|
-
$ ${brand.name} skill install --skip-remotion
|
|
4752
4330
|
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
);
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4331
|
+
// src/commands/skill/installer.ts
|
|
4332
|
+
function validatePath(basePath, targetPath) {
|
|
4333
|
+
const resolvedBase = resolve4(basePath);
|
|
4334
|
+
const resolvedTarget = resolve4(basePath, targetPath);
|
|
4335
|
+
const relativePath = relative(resolvedBase, resolvedTarget);
|
|
4336
|
+
if (relativePath.startsWith("..") || resolve4(resolvedTarget) !== resolvedTarget.replace(/\.\./g, "")) {
|
|
4337
|
+
throw new Error(`Invalid path: "${targetPath}" would escape base directory`);
|
|
4338
|
+
}
|
|
4339
|
+
return resolvedTarget;
|
|
4340
|
+
}
|
|
4341
|
+
function installSkillToPath(skillPath, content) {
|
|
4342
|
+
const skillFile = join(skillPath, "SKILL.md");
|
|
4343
|
+
mkdirSync(skillPath, { recursive: true });
|
|
4344
|
+
writeFileSync(skillFile, content, "utf-8");
|
|
4345
|
+
}
|
|
4346
|
+
function installSkill(skillName, content, options = {}) {
|
|
4347
|
+
const result = {
|
|
4348
|
+
installed: [],
|
|
4349
|
+
skipped: [],
|
|
4350
|
+
errors: []
|
|
4351
|
+
};
|
|
4761
4352
|
const baseDir = options.local ? process.cwd() : homedir();
|
|
4762
|
-
const skillFiles = generateAllSkillFiles(brand);
|
|
4763
4353
|
if (options.dir) {
|
|
4764
|
-
const skillPath = join(options.dir, "skills", brand.name);
|
|
4765
4354
|
try {
|
|
4766
|
-
|
|
4767
|
-
|
|
4355
|
+
const resolvedDir = resolve4(options.dir);
|
|
4356
|
+
const skillPath = validatePath(resolvedDir, join("skills", skillName));
|
|
4357
|
+
installSkillToPath(skillPath, content);
|
|
4358
|
+
result.installed.push(options.dir);
|
|
4768
4359
|
} catch (err) {
|
|
4769
|
-
errors.push(`${options.dir}: ${err instanceof Error ? err.message : String(err)}`);
|
|
4360
|
+
result.errors.push(`${options.dir}: ${err instanceof Error ? err.message : String(err)}`);
|
|
4770
4361
|
}
|
|
4771
4362
|
} else {
|
|
4772
|
-
for (const editor of
|
|
4363
|
+
for (const editor of SUPPORTED_EDITORS) {
|
|
4773
4364
|
const editorDir = join(baseDir, editor.dir);
|
|
4774
|
-
const skillPath = join(editorDir, "skills",
|
|
4365
|
+
const skillPath = join(editorDir, "skills", skillName);
|
|
4775
4366
|
const skillFile = join(skillPath, "SKILL.md");
|
|
4776
4367
|
if (!existsSync2(editorDir)) {
|
|
4777
4368
|
continue;
|
|
4778
4369
|
}
|
|
4779
4370
|
if (existsSync2(skillFile) && !options.force) {
|
|
4780
|
-
skipped.push(editor.name);
|
|
4371
|
+
result.skipped.push(editor.name);
|
|
4781
4372
|
continue;
|
|
4782
4373
|
}
|
|
4783
4374
|
try {
|
|
4784
|
-
|
|
4785
|
-
installed.push(editor.name);
|
|
4375
|
+
installSkillToPath(skillPath, content);
|
|
4376
|
+
result.installed.push(editor.name);
|
|
4786
4377
|
} catch (err) {
|
|
4787
|
-
errors.push(`${editor.name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
4378
|
+
result.errors.push(`${editor.name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
4788
4379
|
}
|
|
4789
4380
|
}
|
|
4790
4381
|
}
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4382
|
+
return result;
|
|
4383
|
+
}
|
|
4384
|
+
function uninstallSkill(skillName, options = {}) {
|
|
4385
|
+
const result = {
|
|
4386
|
+
removed: [],
|
|
4387
|
+
errors: []
|
|
4388
|
+
};
|
|
4389
|
+
const baseDir = options.local ? process.cwd() : homedir();
|
|
4390
|
+
for (const editor of SUPPORTED_EDITORS) {
|
|
4391
|
+
const skillPath = join(baseDir, editor.dir, "skills", skillName);
|
|
4392
|
+
if (existsSync2(skillPath)) {
|
|
4393
|
+
try {
|
|
4394
|
+
rmSync(skillPath, { recursive: true });
|
|
4395
|
+
result.removed.push(editor.name);
|
|
4396
|
+
} catch (err) {
|
|
4397
|
+
result.errors.push(`${editor.name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
4398
|
+
}
|
|
4399
|
+
}
|
|
4797
4400
|
}
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
4401
|
+
return result;
|
|
4402
|
+
}
|
|
4403
|
+
function getSupportedEditorNames() {
|
|
4404
|
+
return SUPPORTED_EDITORS.map((e) => e.name);
|
|
4405
|
+
}
|
|
4406
|
+
|
|
4407
|
+
// src/commands/skill/index.ts
|
|
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(
|
|
4414
|
+
"after",
|
|
4415
|
+
`
|
|
4416
|
+
${chalk12.bold("Examples:")}
|
|
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
|
|
4425
|
+
|
|
4426
|
+
${chalk12.gray("# Install to specific directory")}
|
|
4427
|
+
$ ${brand.commands[0]} skill install video --dir ~/.claude
|
|
4428
|
+
|
|
4429
|
+
${chalk12.gray("# Show skill content")}
|
|
4430
|
+
$ ${brand.commands[0]} skill show video
|
|
4431
|
+
`
|
|
4432
|
+
);
|
|
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
|
+
});
|
|
4802
4440
|
}
|
|
4803
|
-
if (
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
}
|
|
4441
|
+
if (!type || type === "presentation") {
|
|
4442
|
+
skillsToInstall.push({
|
|
4443
|
+
name: `${brand.name}-presentation`,
|
|
4444
|
+
content: generatePresentationSkillContent(skillContext)
|
|
4445
|
+
});
|
|
4808
4446
|
}
|
|
4809
|
-
if (
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
console.log(chalk12.gray("Supported editors: " + EDITORS.map((e) => e.name).join(", ")));
|
|
4813
|
-
console.log(chalk12.gray("Use --dir <path> to install to a specific directory"));
|
|
4447
|
+
if (type && type !== "video" && type !== "presentation") {
|
|
4448
|
+
error(`Invalid skill type: ${type}. Must be "video" or "presentation"`);
|
|
4449
|
+
process.exit(1);
|
|
4814
4450
|
}
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
info(
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
console.log(chalk12.gray(" Run manually: npx skills add remotion-dev/skills"));
|
|
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(", "));
|
|
4827
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
|
+
}
|
|
4477
|
+
console.log();
|
|
4828
4478
|
}
|
|
4829
|
-
console.log();
|
|
4830
4479
|
});
|
|
4831
|
-
skillCommand.command("show").description("Display
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
=== ${path2} ===
|
|
4837
|
-
`));
|
|
4838
|
-
console.log(content);
|
|
4839
|
-
}
|
|
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));
|
|
4840
4485
|
} else {
|
|
4841
|
-
|
|
4842
|
-
|
|
4486
|
+
error(`Invalid skill type: ${type}. Must be "video" or "presentation"`);
|
|
4487
|
+
process.exit(1);
|
|
4843
4488
|
}
|
|
4844
4489
|
});
|
|
4845
|
-
skillCommand.command("uninstall").description(`Remove
|
|
4846
|
-
const
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
}
|
|
4857
|
-
}
|
|
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`);
|
|
4494
|
+
}
|
|
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);
|
|
4858
4501
|
}
|
|
4859
4502
|
console.log();
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
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
|
+
}
|
|
4515
|
+
}
|
|
4865
4516
|
}
|
|
4866
4517
|
console.log();
|
|
4867
4518
|
});
|
|
4868
|
-
function installSkill(skillPath, files, force) {
|
|
4869
|
-
mkdirSync(join(skillPath, "rules"), { recursive: true });
|
|
4870
|
-
mkdirSync(join(skillPath, "assets"), { recursive: true });
|
|
4871
|
-
for (const [relativePath, content] of Object.entries(files)) {
|
|
4872
|
-
const filePath = join(skillPath, relativePath);
|
|
4873
|
-
writeFileSync(filePath, content, "utf-8");
|
|
4874
|
-
}
|
|
4875
|
-
}
|
|
4876
4519
|
|
|
4877
4520
|
// src/commands/tts.ts
|
|
4878
4521
|
init_api();
|
|
@@ -5130,7 +4773,7 @@ async function downloadFile2(url, outputPath) {
|
|
|
5130
4773
|
const buffer = await response.arrayBuffer();
|
|
5131
4774
|
await writeFile4(outputPath, Buffer.from(buffer));
|
|
5132
4775
|
}
|
|
5133
|
-
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) => {
|
|
5134
4777
|
if (!options.music && !options.voice) {
|
|
5135
4778
|
error("At least one of --music or --voice must be provided");
|
|
5136
4779
|
process.exit(EXIT_CODES.INVALID_INPUT);
|
|
@@ -5299,9 +4942,10 @@ init_output();
|
|
|
5299
4942
|
init_types();
|
|
5300
4943
|
import { Command as Command19 } from "commander";
|
|
5301
4944
|
import ora12 from "ora";
|
|
5302
|
-
import { mkdir, writeFile as writeFile5, readFile as readFile2, access, rm } from "fs/promises";
|
|
5303
|
-
import { join as join2, resolve as
|
|
5304
|
-
import { execSync
|
|
4945
|
+
import { mkdir, writeFile as writeFile5, readFile as readFile2, access, rm, cp } from "fs/promises";
|
|
4946
|
+
import { join as join2, resolve as resolve5 } from "path";
|
|
4947
|
+
import { execSync, spawn } from "child_process";
|
|
4948
|
+
import ffmpegPath from "ffmpeg-static";
|
|
5305
4949
|
var DEFAULT_TEMPLATE = "inizio-inc/remotion-composition";
|
|
5306
4950
|
var DEFAULT_FPS = 30;
|
|
5307
4951
|
function parseScriptIntoSections(script) {
|
|
@@ -5391,6 +5035,26 @@ function calculateSectionTimingFromTimestamps(sections, timestamps, fps) {
|
|
|
5391
5035
|
}
|
|
5392
5036
|
return results;
|
|
5393
5037
|
}
|
|
5038
|
+
async function readStdin2() {
|
|
5039
|
+
if (process.stdin.isTTY) {
|
|
5040
|
+
return null;
|
|
5041
|
+
}
|
|
5042
|
+
return new Promise((resolve6) => {
|
|
5043
|
+
let data = "";
|
|
5044
|
+
process.stdin.setEncoding("utf-8");
|
|
5045
|
+
process.stdin.on("data", (chunk) => {
|
|
5046
|
+
data += chunk;
|
|
5047
|
+
});
|
|
5048
|
+
process.stdin.on("end", () => resolve6(data.trim() || null));
|
|
5049
|
+
process.stdin.on("error", () => resolve6(null));
|
|
5050
|
+
setTimeout(() => {
|
|
5051
|
+
if (!data) resolve6(null);
|
|
5052
|
+
}, 100);
|
|
5053
|
+
});
|
|
5054
|
+
}
|
|
5055
|
+
function toFilename(name) {
|
|
5056
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
5057
|
+
}
|
|
5394
5058
|
async function downloadFile3(url, outputPath) {
|
|
5395
5059
|
if (url.startsWith("data:")) {
|
|
5396
5060
|
const matches = url.match(/^data:[^;]+;base64,(.+)$/);
|
|
@@ -5420,72 +5084,207 @@ function getExtension(url) {
|
|
|
5420
5084
|
}
|
|
5421
5085
|
return "jpg";
|
|
5422
5086
|
}
|
|
5423
|
-
var createCommand2 = new Command19("create").description("Create video assets (voiceover, music, images)").option("-s, --script <text>", "Narration script
|
|
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) => {
|
|
5424
5088
|
const format = options.format;
|
|
5425
5089
|
const spinner = format === "human" ? ora12("Initializing...").start() : null;
|
|
5426
5090
|
try {
|
|
5427
|
-
|
|
5428
|
-
|
|
5091
|
+
const stdinData = await readStdin2();
|
|
5092
|
+
let scenesInput = null;
|
|
5093
|
+
if (stdinData) {
|
|
5429
5094
|
try {
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5095
|
+
const parsed = JSON.parse(stdinData);
|
|
5096
|
+
if (parsed.scenes && Array.isArray(parsed.scenes)) {
|
|
5097
|
+
scenesInput = parsed;
|
|
5098
|
+
}
|
|
5099
|
+
} catch {
|
|
5435
5100
|
}
|
|
5436
5101
|
}
|
|
5437
|
-
if (!
|
|
5438
|
-
|
|
5439
|
-
|
|
5440
|
-
|
|
5441
|
-
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
spinner?.stop();
|
|
5447
|
-
error("Number of images must be between 1 and 20");
|
|
5448
|
-
process.exit(EXIT_CODES.INVALID_INPUT);
|
|
5102
|
+
if (!scenesInput && options.scriptFile) {
|
|
5103
|
+
try {
|
|
5104
|
+
const fileContent = await readFile2(options.scriptFile, "utf-8");
|
|
5105
|
+
const parsed = JSON.parse(fileContent);
|
|
5106
|
+
if (parsed.scenes && Array.isArray(parsed.scenes)) {
|
|
5107
|
+
scenesInput = parsed;
|
|
5108
|
+
}
|
|
5109
|
+
} catch {
|
|
5110
|
+
}
|
|
5449
5111
|
}
|
|
5112
|
+
const voice = scenesInput?.voice || options.voice;
|
|
5113
|
+
const musicPrompt = scenesInput?.musicPrompt || options.musicPrompt || "uplifting background music, positive energy";
|
|
5450
5114
|
const audioDir = join2(options.output, "audio");
|
|
5451
5115
|
const imagesDir = join2(options.output, "images");
|
|
5116
|
+
const videosDir = join2(options.output, "videos");
|
|
5452
5117
|
if (spinner) spinner.text = "Creating directories...";
|
|
5453
5118
|
await mkdir(audioDir, { recursive: true });
|
|
5454
5119
|
await mkdir(imagesDir, { recursive: true });
|
|
5120
|
+
await mkdir(videosDir, { recursive: true });
|
|
5455
5121
|
let totalCost = 0;
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
|
|
5472
|
-
|
|
5473
|
-
|
|
5474
|
-
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
|
|
5122
|
+
let scenes = [];
|
|
5123
|
+
let totalDuration = 0;
|
|
5124
|
+
const allImages = [];
|
|
5125
|
+
const allVideos = [];
|
|
5126
|
+
if (scenesInput && scenesInput.scenes.length > 0) {
|
|
5127
|
+
if (format === "human") {
|
|
5128
|
+
spinner?.stop();
|
|
5129
|
+
info(`Processing ${scenesInput.scenes.length} scenes...`);
|
|
5130
|
+
spinner?.start();
|
|
5131
|
+
}
|
|
5132
|
+
let currentTime = 0;
|
|
5133
|
+
for (let i = 0; i < scenesInput.scenes.length; i++) {
|
|
5134
|
+
const scene = scenesInput.scenes[i];
|
|
5135
|
+
const filename = toFilename(scene.name);
|
|
5136
|
+
if (spinner) spinner.text = `[${scene.name}] Generating speech...`;
|
|
5137
|
+
const ttsResult = await generateSpeech({
|
|
5138
|
+
text: scene.script,
|
|
5139
|
+
options: {
|
|
5140
|
+
voice,
|
|
5141
|
+
voiceSettings: scenesInput.voiceSettings
|
|
5142
|
+
}
|
|
5143
|
+
});
|
|
5144
|
+
const audioPath = join2(audioDir, `${filename}.${ttsResult.format}`);
|
|
5145
|
+
await writeFile5(audioPath, ttsResult.audioData);
|
|
5146
|
+
totalCost += ttsResult.cost;
|
|
5147
|
+
const durationInSeconds = ttsResult.duration;
|
|
5148
|
+
const durationInFrames = Math.round(durationInSeconds * DEFAULT_FPS);
|
|
5149
|
+
const sceneData = {
|
|
5150
|
+
id: i + 1,
|
|
5151
|
+
name: scene.name,
|
|
5152
|
+
text: scene.script,
|
|
5153
|
+
wordCount: scene.script.split(/\s+/).length,
|
|
5154
|
+
startTime: currentTime,
|
|
5155
|
+
endTime: currentTime + durationInSeconds,
|
|
5156
|
+
durationInSeconds,
|
|
5157
|
+
durationInFrames,
|
|
5158
|
+
audioPath: `audio/${filename}.${ttsResult.format}`
|
|
5159
|
+
};
|
|
5160
|
+
if (scene.imageQuery) {
|
|
5161
|
+
if (spinner) spinner.text = `[${scene.name}] Searching image...`;
|
|
5162
|
+
try {
|
|
5163
|
+
const imageResults = await searchImages({
|
|
5164
|
+
query: scene.imageQuery,
|
|
5165
|
+
options: { maxResults: 1, size: "large", safeSearch: true }
|
|
5166
|
+
});
|
|
5167
|
+
const imgs = imageResults.data.results.flatMap((r) => r.results);
|
|
5168
|
+
totalCost += imageResults.data.totalCost;
|
|
5169
|
+
if (imgs.length > 0) {
|
|
5170
|
+
const img = imgs[0];
|
|
5171
|
+
const ext = getExtension(img.url);
|
|
5172
|
+
const imgFilename = `${filename}.${ext}`;
|
|
5173
|
+
const imgPath = join2(imagesDir, imgFilename);
|
|
5174
|
+
await downloadFile3(img.url, imgPath);
|
|
5175
|
+
sceneData.imagePath = `images/${imgFilename}`;
|
|
5176
|
+
allImages.push({
|
|
5177
|
+
path: `images/${imgFilename}`,
|
|
5178
|
+
url: img.url,
|
|
5179
|
+
width: img.width,
|
|
5180
|
+
height: img.height,
|
|
5181
|
+
query: scene.imageQuery
|
|
5182
|
+
});
|
|
5183
|
+
}
|
|
5184
|
+
} catch (err) {
|
|
5185
|
+
if (format === "human") {
|
|
5186
|
+
spinner?.stop();
|
|
5187
|
+
warn(`[${scene.name}] Image search failed: ${err instanceof Error ? err.message : "Unknown"}`);
|
|
5188
|
+
spinner?.start();
|
|
5189
|
+
}
|
|
5190
|
+
}
|
|
5191
|
+
}
|
|
5192
|
+
if (scene.videoQuery) {
|
|
5193
|
+
if (spinner) spinner.text = `[${scene.name}] Searching video...`;
|
|
5194
|
+
try {
|
|
5195
|
+
const videoResults = await searchVideos({
|
|
5196
|
+
query: scene.videoQuery,
|
|
5197
|
+
options: { maxResults: 1, license: "free" }
|
|
5198
|
+
});
|
|
5199
|
+
const vids = videoResults.data.results.flatMap((r) => r.results);
|
|
5200
|
+
totalCost += videoResults.data.totalCost;
|
|
5201
|
+
if (vids.length > 0) {
|
|
5202
|
+
const vid = vids[0];
|
|
5203
|
+
const vidUrl = vid.previewUrl || vid.downloadUrl;
|
|
5204
|
+
if (vidUrl) {
|
|
5205
|
+
const vidFilename = `${filename}.mp4`;
|
|
5206
|
+
const vidPath = join2(videosDir, vidFilename);
|
|
5207
|
+
await downloadFile3(vidUrl, vidPath);
|
|
5208
|
+
sceneData.videoPath = `videos/${vidFilename}`;
|
|
5209
|
+
allVideos.push({
|
|
5210
|
+
path: `videos/${vidFilename}`,
|
|
5211
|
+
url: vidUrl,
|
|
5212
|
+
width: vid.width,
|
|
5213
|
+
height: vid.height,
|
|
5214
|
+
duration: vid.duration,
|
|
5215
|
+
query: scene.videoQuery
|
|
5216
|
+
});
|
|
5217
|
+
}
|
|
5218
|
+
}
|
|
5219
|
+
} catch (err) {
|
|
5220
|
+
if (format === "human") {
|
|
5221
|
+
spinner?.stop();
|
|
5222
|
+
warn(`[${scene.name}] Video search failed: ${err instanceof Error ? err.message : "Unknown"}`);
|
|
5223
|
+
spinner?.start();
|
|
5224
|
+
}
|
|
5225
|
+
}
|
|
5226
|
+
}
|
|
5227
|
+
scenes.push(sceneData);
|
|
5228
|
+
currentTime += durationInSeconds;
|
|
5229
|
+
totalDuration += durationInSeconds;
|
|
5230
|
+
if (format === "human") {
|
|
5231
|
+
spinner?.stop();
|
|
5232
|
+
const assets = [
|
|
5233
|
+
`audio: ${durationInSeconds.toFixed(1)}s`,
|
|
5234
|
+
sceneData.imagePath ? "image" : null,
|
|
5235
|
+
sceneData.videoPath ? "video" : null
|
|
5236
|
+
].filter(Boolean).join(", ");
|
|
5237
|
+
success(` ${scene.name}: ${assets}`);
|
|
5238
|
+
spinner?.start();
|
|
5239
|
+
}
|
|
5240
|
+
}
|
|
5241
|
+
} else {
|
|
5242
|
+
let script = options.script;
|
|
5243
|
+
if (options.scriptFile) {
|
|
5244
|
+
try {
|
|
5245
|
+
script = await readFile2(options.scriptFile, "utf-8");
|
|
5246
|
+
} catch (err) {
|
|
5247
|
+
spinner?.stop();
|
|
5248
|
+
error(`Failed to read script file: ${err instanceof Error ? err.message : "Unknown error"}`);
|
|
5249
|
+
process.exit(EXIT_CODES.INVALID_INPUT);
|
|
5250
|
+
}
|
|
5251
|
+
}
|
|
5252
|
+
if (!script || script.trim().length === 0) {
|
|
5253
|
+
spinner?.stop();
|
|
5254
|
+
error("Provide scenes via stdin JSON, --script-file with scenes JSON, or --script for legacy mode");
|
|
5255
|
+
process.exit(EXIT_CODES.INVALID_INPUT);
|
|
5256
|
+
}
|
|
5257
|
+
script = script.trim();
|
|
5258
|
+
const _topic = options.topic || script.split(".")[0].slice(0, 50);
|
|
5259
|
+
void _topic;
|
|
5260
|
+
if (spinner) spinner.text = "Generating voiceover...";
|
|
5261
|
+
const ttsResult = await generateSpeech({
|
|
5262
|
+
text: script,
|
|
5263
|
+
options: { voice }
|
|
5264
|
+
});
|
|
5265
|
+
const voiceoverPath = join2(audioDir, `voiceover.${ttsResult.format}`);
|
|
5266
|
+
await writeFile5(voiceoverPath, ttsResult.audioData);
|
|
5267
|
+
totalCost += ttsResult.cost;
|
|
5268
|
+
totalDuration = ttsResult.duration;
|
|
5269
|
+
const sectionTexts = parseScriptIntoSections(script);
|
|
5270
|
+
const sectionsWithTiming = calculateSectionTiming(sectionTexts, ttsResult.duration, DEFAULT_FPS, ttsResult.timestamps);
|
|
5271
|
+
scenes = sectionsWithTiming.map((s, i) => ({
|
|
5272
|
+
...s,
|
|
5273
|
+
name: `Section${i + 1}`,
|
|
5274
|
+
audioPath: `audio/voiceover.${ttsResult.format}`
|
|
5275
|
+
}));
|
|
5276
|
+
if (format === "human") {
|
|
5277
|
+
spinner?.stop();
|
|
5278
|
+
success(`Voiceover: ${voiceoverPath} (${ttsResult.duration.toFixed(1)}s)`);
|
|
5279
|
+
spinner?.start();
|
|
5280
|
+
}
|
|
5486
5281
|
}
|
|
5487
|
-
const musicDuration = Math.min(30, Math.ceil(
|
|
5488
|
-
|
|
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
|
+
});
|
|
5489
5288
|
if (spinner) spinner.text = "Generating music...";
|
|
5490
5289
|
let musicResult = await generateMusic({
|
|
5491
5290
|
prompt: musicPrompt,
|
|
@@ -5509,78 +5308,37 @@ var createCommand2 = new Command19("create").description("Create video assets (v
|
|
|
5509
5308
|
await downloadFile3(musicResult.audioUrl, musicPath);
|
|
5510
5309
|
}
|
|
5511
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
|
+
});
|
|
5512
5320
|
const musicInfo = {
|
|
5513
5321
|
path: "audio/music.mp3",
|
|
5514
|
-
duration:
|
|
5322
|
+
duration: actualMusicDuration,
|
|
5515
5323
|
prompt: musicPrompt,
|
|
5516
5324
|
cost: musicResult.cost || 0
|
|
5517
5325
|
};
|
|
5518
5326
|
if (format === "human") {
|
|
5519
5327
|
spinner?.stop();
|
|
5520
5328
|
success(`Music: ${musicPath} (${musicInfo.duration}s)`);
|
|
5521
|
-
|
|
5522
|
-
|
|
5523
|
-
|
|
5524
|
-
const imageResults = await searchImages({
|
|
5525
|
-
query: topic,
|
|
5526
|
-
options: {
|
|
5527
|
-
maxResults: numImages,
|
|
5528
|
-
size: "large",
|
|
5529
|
-
safeSearch: true
|
|
5530
|
-
}
|
|
5531
|
-
});
|
|
5532
|
-
const allImages = imageResults.data.results.flatMap(
|
|
5533
|
-
(providerResult) => providerResult.results.map((img) => ({
|
|
5534
|
-
...img,
|
|
5535
|
-
provider: providerResult.providerName
|
|
5536
|
-
}))
|
|
5537
|
-
);
|
|
5538
|
-
totalCost += imageResults.data.totalCost;
|
|
5539
|
-
const downloadedImages = [];
|
|
5540
|
-
for (let i = 0; i < Math.min(allImages.length, numImages); i++) {
|
|
5541
|
-
const img = allImages[i];
|
|
5542
|
-
const ext = getExtension(img.url);
|
|
5543
|
-
const filename = `scene-${i + 1}.${ext}`;
|
|
5544
|
-
const imagePath = join2(imagesDir, filename);
|
|
5545
|
-
if (spinner) spinner.text = `Downloading image ${i + 1}/${Math.min(allImages.length, numImages)}...`;
|
|
5546
|
-
try {
|
|
5547
|
-
await downloadFile3(img.url, imagePath);
|
|
5548
|
-
downloadedImages.push({
|
|
5549
|
-
path: `images/${filename}`,
|
|
5550
|
-
url: img.url,
|
|
5551
|
-
width: img.width,
|
|
5552
|
-
height: img.height,
|
|
5553
|
-
query: topic
|
|
5554
|
-
});
|
|
5555
|
-
} catch (err) {
|
|
5556
|
-
if (format === "human") {
|
|
5557
|
-
spinner?.stop();
|
|
5558
|
-
warn(`Failed to download image ${i + 1}: ${err instanceof Error ? err.message : "Unknown error"}`);
|
|
5559
|
-
spinner?.start();
|
|
5560
|
-
}
|
|
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.`);
|
|
5561
5332
|
}
|
|
5562
|
-
}
|
|
5563
|
-
if (format === "human") {
|
|
5564
|
-
spinner?.stop();
|
|
5565
|
-
success(`Images: Downloaded ${downloadedImages.length} images to ${imagesDir}`);
|
|
5566
5333
|
spinner?.start();
|
|
5567
5334
|
}
|
|
5568
|
-
const sectionsWithImages = sections.map((section, index) => {
|
|
5569
|
-
const imageIndex = index % downloadedImages.length;
|
|
5570
|
-
return {
|
|
5571
|
-
...section,
|
|
5572
|
-
imagePath: downloadedImages[imageIndex]?.path
|
|
5573
|
-
};
|
|
5574
|
-
});
|
|
5575
5335
|
if (spinner) spinner.text = "Writing manifest...";
|
|
5576
|
-
const totalDurationInFrames = Math.round(
|
|
5336
|
+
const totalDurationInFrames = Math.round(totalDuration * DEFAULT_FPS);
|
|
5577
5337
|
const manifest = {
|
|
5578
|
-
topic,
|
|
5579
|
-
script,
|
|
5580
|
-
voiceover: voiceoverInfo,
|
|
5581
5338
|
music: musicInfo,
|
|
5582
|
-
images:
|
|
5583
|
-
|
|
5339
|
+
images: allImages,
|
|
5340
|
+
videos: allVideos,
|
|
5341
|
+
scenes,
|
|
5584
5342
|
totalDurationInFrames,
|
|
5585
5343
|
fps: DEFAULT_FPS,
|
|
5586
5344
|
totalCost,
|
|
@@ -5600,26 +5358,26 @@ var createCommand2 = new Command19("create").description("Create video assets (v
|
|
|
5600
5358
|
console.log();
|
|
5601
5359
|
success("Video assets created successfully!");
|
|
5602
5360
|
console.log();
|
|
5603
|
-
info(`
|
|
5604
|
-
|
|
5361
|
+
info(`Scenes: ${scenes.length} (${totalDurationInFrames} frames at ${DEFAULT_FPS}fps)`);
|
|
5362
|
+
for (const scene of scenes) {
|
|
5363
|
+
const assets = [
|
|
5364
|
+
scene.audioPath ? "audio" : null,
|
|
5365
|
+
scene.imagePath ? "image" : null,
|
|
5366
|
+
scene.videoPath ? "video" : null
|
|
5367
|
+
].filter(Boolean).join(", ");
|
|
5368
|
+
info(` - ${scene.name}: ${scene.durationInSeconds.toFixed(1)}s [${assets}]`);
|
|
5369
|
+
}
|
|
5605
5370
|
info(`Music: ${musicInfo.path} (${musicInfo.duration}s)`);
|
|
5606
|
-
info(`Sections: ${sections.length} (${totalDurationInFrames} frames at ${DEFAULT_FPS}fps)`);
|
|
5607
|
-
info(`Images: ${downloadedImages.length} downloaded`);
|
|
5608
5371
|
info(`Manifest: ${manifestPath}`);
|
|
5609
5372
|
console.log();
|
|
5610
5373
|
info(`Total cost: $${totalCost.toFixed(4)}`);
|
|
5611
|
-
console.log();
|
|
5612
|
-
info("Next steps:");
|
|
5613
|
-
info(" 1. Create Remotion scenes matching section timings in manifest");
|
|
5614
|
-
info(" 2. Each section has exact durationInFrames - use these for sync");
|
|
5615
|
-
info(" 3. Run: npx remotion render FullVideo out/video.mp4");
|
|
5616
5374
|
} catch (err) {
|
|
5617
5375
|
spinner?.stop();
|
|
5618
5376
|
error(err instanceof Error ? err.message : "Unknown error");
|
|
5619
5377
|
process.exit(EXIT_CODES.GENERAL_ERROR);
|
|
5620
5378
|
}
|
|
5621
5379
|
});
|
|
5622
|
-
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) => {
|
|
5623
5381
|
const { maxResults, orientation, license, format } = options;
|
|
5624
5382
|
const spinner = format === "human" ? ora12("Searching for videos...").start() : null;
|
|
5625
5383
|
try {
|
|
@@ -5663,11 +5421,11 @@ var searchCommand2 = new Command19("search").description("Search for stock video
|
|
|
5663
5421
|
process.exit(EXIT_CODES.GENERAL_ERROR);
|
|
5664
5422
|
}
|
|
5665
5423
|
});
|
|
5666
|
-
var initCommand = new Command19("init").description("Create a new Remotion video project from template").argument("<name>", "Project directory name").option("-t, --template <repo>", "GitHub repo (user/repo)", DEFAULT_TEMPLATE).option("--no-install", "Skip pnpm install").option("-f, --format <format>", "Output format: human, json, quiet", "human").action(async (name, options) => {
|
|
5424
|
+
var initCommand = new Command19("init").description("Create a new Remotion video project from template").argument("<name>", "Project directory name").option("-t, --template <repo>", "GitHub repo (user/repo)", DEFAULT_TEMPLATE).option("--type <type>", "Video type: landscape (16:9) or tiktok (9:16)", "landscape").option("--no-install", "Skip pnpm install").option("-f, --format <format>", "Output format: human, json, quiet", "human").action(async (name, options) => {
|
|
5667
5425
|
const format = options.format;
|
|
5668
5426
|
const spinner = format === "human" ? ora12("Initializing video project...").start() : null;
|
|
5669
5427
|
try {
|
|
5670
|
-
const targetDir =
|
|
5428
|
+
const targetDir = resolve5(process.cwd(), name);
|
|
5671
5429
|
try {
|
|
5672
5430
|
await access(targetDir);
|
|
5673
5431
|
spinner?.stop();
|
|
@@ -5675,14 +5433,22 @@ var initCommand = new Command19("init").description("Create a new Remotion video
|
|
|
5675
5433
|
process.exit(EXIT_CODES.INVALID_INPUT);
|
|
5676
5434
|
} catch {
|
|
5677
5435
|
}
|
|
5436
|
+
const templatePattern = /^[a-zA-Z0-9_-]+\/[a-zA-Z0-9_.-]+(\/[a-zA-Z0-9_.-]+)*(#[a-zA-Z0-9_.-]+)?$/;
|
|
5437
|
+
if (!templatePattern.test(options.template)) {
|
|
5438
|
+
spinner?.stop();
|
|
5439
|
+
error(`Invalid template format: "${options.template}". Expected format: owner/repo or owner/repo#branch`);
|
|
5440
|
+
process.exit(EXIT_CODES.INVALID_INPUT);
|
|
5441
|
+
}
|
|
5678
5442
|
if (spinner) spinner.text = `Downloading template from ${options.template}...`;
|
|
5679
5443
|
try {
|
|
5680
|
-
|
|
5444
|
+
execSync(`npx --yes degit ${options.template} "${targetDir}"`, {
|
|
5681
5445
|
stdio: "pipe"
|
|
5682
5446
|
});
|
|
5683
5447
|
} catch {
|
|
5684
5448
|
if (spinner) spinner.text = "Cloning template...";
|
|
5685
|
-
|
|
5449
|
+
const repoMatch = options.template.match(/^([a-zA-Z0-9_-]+\/[a-zA-Z0-9_.-]+)/);
|
|
5450
|
+
const repo = repoMatch ? repoMatch[1] : options.template;
|
|
5451
|
+
execSync(`git clone --depth 1 https://github.com/${repo}.git "${targetDir}"`, {
|
|
5686
5452
|
stdio: "pipe"
|
|
5687
5453
|
});
|
|
5688
5454
|
await rm(join2(targetDir, ".git"), { recursive: true, force: true });
|
|
@@ -5715,12 +5481,37 @@ var initCommand = new Command19("init").description("Create a new Remotion video
|
|
|
5715
5481
|
spinner?.start();
|
|
5716
5482
|
}
|
|
5717
5483
|
}
|
|
5484
|
+
if (options.type === "tiktok") {
|
|
5485
|
+
if (spinner) spinner.text = "Configuring for TikTok (9:16)...";
|
|
5486
|
+
const constantsPath = join2(targetDir, "types/constants.ts");
|
|
5487
|
+
try {
|
|
5488
|
+
let constantsContent = await readFile2(constantsPath, "utf-8");
|
|
5489
|
+
constantsContent = constantsContent.replace(
|
|
5490
|
+
/export const VIDEO_WIDTH = 1920;/,
|
|
5491
|
+
"export const VIDEO_WIDTH = 1080; // TikTok 9:16"
|
|
5492
|
+
).replace(
|
|
5493
|
+
/export const VIDEO_HEIGHT = 1080;/,
|
|
5494
|
+
"export const VIDEO_HEIGHT = 1920; // TikTok 9:16"
|
|
5495
|
+
).replace(
|
|
5496
|
+
/export const VIDEO_FPS = 60;/,
|
|
5497
|
+
"export const VIDEO_FPS = 30; // TikTok standard"
|
|
5498
|
+
);
|
|
5499
|
+
await writeFile5(constantsPath, constantsContent, "utf-8");
|
|
5500
|
+
if (format === "human") {
|
|
5501
|
+
spinner?.stop();
|
|
5502
|
+
success("Configured for TikTok (1080x1920 @ 30fps)");
|
|
5503
|
+
spinner?.start();
|
|
5504
|
+
}
|
|
5505
|
+
} catch {
|
|
5506
|
+
}
|
|
5507
|
+
}
|
|
5718
5508
|
spinner?.stop();
|
|
5719
5509
|
if (format === "json") {
|
|
5720
5510
|
printJson({
|
|
5721
5511
|
name,
|
|
5722
5512
|
path: targetDir,
|
|
5723
5513
|
template: options.template,
|
|
5514
|
+
type: options.type,
|
|
5724
5515
|
installed: options.install
|
|
5725
5516
|
});
|
|
5726
5517
|
return;
|
|
@@ -5731,6 +5522,11 @@ var initCommand = new Command19("init").description("Create a new Remotion video
|
|
|
5731
5522
|
}
|
|
5732
5523
|
console.log();
|
|
5733
5524
|
success(`Video project "${name}" created successfully!`);
|
|
5525
|
+
if (options.type === "tiktok") {
|
|
5526
|
+
info("Format: TikTok/Reels/Shorts (1080x1920 @ 30fps)");
|
|
5527
|
+
} else {
|
|
5528
|
+
info("Format: Landscape (1920x1080 @ 60fps)");
|
|
5529
|
+
}
|
|
5734
5530
|
console.log();
|
|
5735
5531
|
info("Next steps:");
|
|
5736
5532
|
info(` cd ${name}`);
|
|
@@ -5739,17 +5535,158 @@ var initCommand = new Command19("init").description("Create a new Remotion video
|
|
|
5739
5535
|
}
|
|
5740
5536
|
info(" pnpm dev # Preview in Remotion Studio");
|
|
5741
5537
|
info(" cc video create ... # Generate assets to public/");
|
|
5742
|
-
|
|
5538
|
+
if (options.type === "tiktok") {
|
|
5539
|
+
info(" pnpm exec remotion render TikTokVideo # Render TikTok video");
|
|
5540
|
+
} else {
|
|
5541
|
+
info(" pnpm exec remotion render FullVideo # Render final video");
|
|
5542
|
+
}
|
|
5743
5543
|
} catch (err) {
|
|
5744
5544
|
spinner?.stop();
|
|
5745
5545
|
error(err instanceof Error ? err.message : "Unknown error");
|
|
5746
5546
|
process.exit(EXIT_CODES.GENERAL_ERROR);
|
|
5747
5547
|
}
|
|
5748
5548
|
});
|
|
5749
|
-
|
|
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);
|
|
5750
5687
|
|
|
5751
5688
|
// src/index.ts
|
|
5752
|
-
var VERSION = "0.1.
|
|
5689
|
+
var VERSION = "0.1.8";
|
|
5753
5690
|
var program = new Command20();
|
|
5754
5691
|
var cmdName = brand.commands[0];
|
|
5755
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({
|