@conceptcraft/mindframes 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js 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.CC_MINDFRAMES_API_KEY ?? process.env.CONCEPTCRAFT_API_KEY;
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.NEXT_PUBLIC_API_URL ?? process.env.CONCEPTCRAFT_API_URL;
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: "mindframes",
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
- "Not authenticated. Run 'mindframes login' or set CC_MINDFRAMES_API_KEY environment variable.",
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
- "Not authenticated. Run 'mindframes login' or set CC_MINDFRAMES_API_KEY environment variable.",
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
- "Not authenticated. Run 'mindframes login' or set CC_MINDFRAMES_API_KEY environment variable.",
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
- "Not authenticated. Run 'mindframes login' or set CC_MINDFRAMES_API_KEY environment variable.",
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
- "Not authenticated. Run 'mindframes login' or set CC_MINDFRAMES_API_KEY environment variable.",
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
- "Not authenticated. Run 'mindframes login' or set CC_MINDFRAMES_API_KEY environment variable.",
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
- "Not authenticated. Run 'cc login' or set CC_SLIDES_API_KEY environment variable.",
969
+ `Not authenticated. Run '${brand.commands[0]} login' or set ${brand.apiKeyEnvVar} environment variable.`,
911
970
  401,
912
971
  2
913
972
  );
@@ -1035,6 +1094,7 @@ var init_api = __esm({
1035
1094
  "use strict";
1036
1095
  init_config();
1037
1096
  init_auth();
1097
+ init_brand();
1038
1098
  ApiError = class extends Error {
1039
1099
  constructor(message, statusCode, exitCode = 1) {
1040
1100
  super(message);
@@ -1395,60 +1455,11 @@ var init_login = __esm({
1395
1455
  });
1396
1456
 
1397
1457
  // src/index.ts
1458
+ init_brand();
1459
+ init_login();
1398
1460
  import { Command as Command20 } from "commander";
1399
1461
  import chalk13 from "chalk";
1400
1462
 
1401
- // src/lib/brand.ts
1402
- import path from "path";
1403
- var BRANDS = {
1404
- conceptcraft: {
1405
- id: "conceptcraft",
1406
- name: "conceptcraft",
1407
- displayName: "ConceptCraft",
1408
- description: "CLI tool for ConceptCraft presentation generation",
1409
- commands: ["cc", "conceptcraft"],
1410
- apiUrl: "https://conceptcraft.ai",
1411
- docsUrl: "https://docs.conceptcraft.ai",
1412
- packageName: "@conceptcraft/cli",
1413
- configDir: ".conceptcraft"
1414
- },
1415
- mindframes: {
1416
- id: "mindframes",
1417
- name: "mindframes",
1418
- displayName: "Mindframes",
1419
- description: "CLI tool for Mindframes presentation generation",
1420
- commands: ["mf", "mindframes"],
1421
- apiUrl: "https://mindframes.app",
1422
- docsUrl: "https://docs.mindframes.app",
1423
- packageName: "@mindframes/cli",
1424
- configDir: ".mindframes"
1425
- }
1426
- };
1427
- var COMMAND_TO_BRAND = {
1428
- cc: "conceptcraft",
1429
- conceptcraft: "conceptcraft",
1430
- mf: "mindframes",
1431
- mindframes: "mindframes"
1432
- };
1433
- function detectBrand() {
1434
- const binaryPath = process.argv[1] || "";
1435
- const binaryName = path.basename(binaryPath).replace(/\.(js|ts)$/, "");
1436
- const brandId = COMMAND_TO_BRAND[binaryName];
1437
- if (brandId) {
1438
- return BRANDS[brandId];
1439
- }
1440
- for (const [cmd2, id] of Object.entries(COMMAND_TO_BRAND)) {
1441
- if (binaryPath.includes(`/${cmd2}`) || binaryPath.includes(`\\${cmd2}`)) {
1442
- return BRANDS[id];
1443
- }
1444
- }
1445
- return BRANDS.mindframes;
1446
- }
1447
- var brand = detectBrand();
1448
-
1449
- // src/index.ts
1450
- init_login();
1451
-
1452
1463
  // src/commands/logout.ts
1453
1464
  init_config();
1454
1465
  init_feature_cache();
@@ -3129,539 +3140,527 @@ var whoamiCommand = new Command13("whoami").description("Show current user and t
3129
3140
 
3130
3141
  // src/commands/skill/index.ts
3131
3142
  init_output();
3143
+ init_brand();
3132
3144
  import { Command as Command14 } from "commander";
3133
3145
  import chalk12 from "chalk";
3134
3146
 
3135
- // src/commands/skill/sections/frontmatter.ts
3136
- var frontmatter = {
3137
- title: "Frontmatter",
3138
- render: (ctx) => `---
3139
- name: ${ctx.cmd}
3140
- description: Use when user asks to create presentations (slides, decks, pitch decks) or videos (product demos, explainers, social content, promos). Also handles voiceover and music generation.
3141
- ---`
3142
- };
3143
-
3144
- // src/commands/skill/sections/header.ts
3145
- var header3 = {
3146
- title: "Header",
3147
- render: (ctx) => `# ${ctx.name} CLI
3148
-
3149
- Create professional presentations directly from your terminal. The CLI generates AI-powered slides from any context you provide - text, files, URLs, or piped content.`
3150
- };
3151
-
3152
- // src/commands/skill/sections/prerequisites.ts
3153
- var prerequisites = {
3154
- title: "Prerequisites",
3155
- render: (ctx) => `## Prerequisites
3147
+ // src/commands/skill/generate-main-skill.ts
3148
+ function generateMainSkillContent(context) {
3149
+ const { name, cmd: cmd2, displayName } = context;
3150
+ const envPrefix = name.toUpperCase().replace(/-/g, "_");
3151
+ return `---
3152
+ name: ${name}
3153
+ description: ${displayName} CLI for AI-powered content creation. Use when user needs to create presentations, generate video assets (voiceover, music, images, stock videos), use text-to-speech, mix audio, search stock media, or manage branding. This is the main entry point - load specialized skills (${name}-video, ${name}-presentation) for detailed workflows.
3154
+ ---
3156
3155
 
3157
- \`\`\`bash
3158
- npm install -g ${ctx.pkg}
3159
- ${ctx.cmd} login # Authenticate (opens browser)
3160
- ${ctx.cmd} whoami # Verify auth
3161
- \`\`\`
3156
+ # ${displayName} CLI
3162
3157
 
3163
- ### Authentication
3158
+ A comprehensive CLI for AI-powered content creation. Generate presentations, video assets, voiceovers, music, and search stock media - all from your terminal.
3164
3159
 
3165
- If not authenticated, run \`${ctx.cmd} login\` - it opens browser for OAuth.
3166
- If login fails or user declines, fall back to API key: \`${ctx.cmd} config init\``
3167
- };
3160
+ **Install:** \`npm install -g @${name}/cli\` or \`pnpm add -g @${name}/cli\`
3168
3161
 
3169
- // src/commands/skill/sections/workflow.ts
3170
- var workflow = {
3171
- title: "Core Workflow",
3172
- render: (ctx) => `## Core Workflow
3162
+ ---
3173
3163
 
3174
- 1. **Gather context** - Read relevant files, code, or documentation
3175
- 2. **Create presentation** - Pass context to \`${ctx.cmd} create\`
3176
- 3. **Share URL** - Return the presentation link to the user`
3177
- };
3164
+ ## Quick Reference
3178
3165
 
3179
- // src/commands/skill/sections/create-command.ts
3180
- var createCommand2 = {
3181
- title: "Create Command",
3182
- render: (ctx) => `## Commands
3166
+ | Task | Command |
3167
+ |------|---------|
3168
+ | Create presentation | \`${cmd2} create "Topic"\` |
3169
+ | Generate video assets | \`${cmd2} video create < scenes.json\` |
3170
+ | Text-to-speech | \`${cmd2} tts generate -t "Text" -o voice.mp3\` |
3171
+ | Generate music | \`${cmd2} music generate -p "upbeat corporate"\` |
3172
+ | Search images | \`${cmd2} image search -q "mountain landscape"\` |
3173
+ | Search videos | \`${cmd2} video search "ocean waves"\` |
3174
+ | Mix audio tracks | \`${cmd2} mix create --video v.mp4 --music m.mp3\` |
3183
3175
 
3184
- ### Create Presentation
3176
+ ---
3185
3177
 
3186
- Context is **required**. Provide it via one of these methods:
3178
+ ## Authentication
3187
3179
 
3188
3180
  \`\`\`bash
3189
- # Upload files (PDFs, PPTX, images, docs)
3190
- ${ctx.cmd} create "Product Overview" --file ./deck.pptx --file ./logo.png
3181
+ # Login via OAuth (recommended)
3182
+ ${cmd2} login
3191
3183
 
3192
- # Direct text context
3193
- ${ctx.cmd} create "Topic Title" --context "Key points, data, facts..."
3184
+ # Or set API key directly
3185
+ export ${envPrefix}_API_KEY="your-key-here"
3194
3186
 
3195
- # From a text file
3196
- ${ctx.cmd} create "Topic Title" --context-file ./notes.md
3187
+ # Verify authentication
3188
+ ${cmd2} whoami
3189
+ \`\`\`
3197
3190
 
3198
- # Pipe content (auto-detected)
3199
- cat README.md | ${ctx.cmd} create "Project Overview"
3191
+ ---
3200
3192
 
3201
- # From URLs (scraped automatically)
3202
- ${ctx.cmd} create "Competitor Analysis" --sources https://example.com/report
3193
+ ## 1. Presentations
3203
3194
 
3204
- # Combine multiple sources
3205
- cat src/auth/*.ts | ${ctx.cmd} create "Auth System" \\
3206
- --file ./architecture.png \\
3207
- --context "Focus on security patterns"
3208
- \`\`\``
3209
- };
3195
+ Create AI-powered presentations from text, files, URLs, or piped content.
3210
3196
 
3211
- // src/commands/skill/sections/create-options.ts
3212
- var createOptions = {
3213
- title: "Create Options",
3214
- render: (_ctx) => `### Create Options
3215
-
3216
- | Option | Description | Default |
3217
- |--------|-------------|---------|
3218
- | \`-n, --slides <count>\` | Number of slides (1-20) | 10 |
3219
- | \`-m, --mode <mode>\` | Quality: \`instant\`, \`fast\`, \`balanced\`, \`best\` | balanced |
3220
- | \`-t, --tone <tone>\` | Tone: \`professional\`, \`educational\`, \`creative\`, \`formal\`, \`casual\` | professional |
3221
- | \`--amount <amount>\` | Density: \`minimal\`, \`concise\`, \`detailed\`, \`extensive\` | concise |
3222
- | \`--audience <text>\` | Target audience | General Audience |
3223
- | \`-g, --goal <type>\` | Purpose: \`inform\`, \`persuade\`, \`train\`, \`learn\`, \`entertain\`, \`report\` | - |
3224
- | \`--custom-goal <text>\` | Custom goal description | - |
3225
- | \`-f, --file <paths...>\` | Files to upload (PDF, PPTX, images, docs) | - |
3226
- | \`-l, --language <lang>\` | Output language | en |
3227
- | \`-b, --brand <id>\` | Branding ID to apply | - |
3228
- | \`-o, --output <format>\` | Output: \`human\`, \`json\`, \`quiet\` | human |`
3229
- };
3197
+ \`\`\`bash
3198
+ # From topic
3199
+ ${cmd2} create "AI-powered analytics platform"
3200
+
3201
+ # From file (PDF, PPTX, DOCX, Markdown)
3202
+ ${cmd2} create "Quarterly Report" --file report.pdf
3203
+
3204
+ # From URL
3205
+ ${cmd2} create "Product Overview" --sources https://company.com/product
3206
+
3207
+ # From piped content
3208
+ cat notes.md | ${cmd2} create "Meeting Summary"
3209
+
3210
+ # With options
3211
+ ${cmd2} create "Pitch Deck" \\
3212
+ --slides 10 \\
3213
+ --mode balanced \\
3214
+ --tone professional \\
3215
+ --audience "Investors" \\
3216
+ --brand my-company \\
3217
+ --open
3218
+ \`\`\`
3230
3219
 
3231
- // src/commands/skill/sections/other-commands.ts
3232
- var otherCommands = {
3233
- title: "Other Commands",
3234
- render: (ctx) => `### Other Commands
3220
+ **Key Options:**
3221
+ - \`--slides <1-20>\` - Number of slides (default: 10)
3222
+ - \`--mode <instant|ultrafast|fast|balanced|best>\` - Quality/speed tradeoff
3223
+ - \`--tone <creative|professional|educational|formal|casual>\`
3224
+ - \`--file <paths...>\` - Extract content from files
3225
+ - \`--sources <urls...>\` - Scrape URLs for context
3226
+ - \`--brand <id|url>\` - Apply branding
3227
+ - \`--open\` - Open in browser when done
3235
3228
 
3229
+ **Manage presentations:**
3236
3230
  \`\`\`bash
3237
- # Check authentication
3238
- ${ctx.cmd} whoami
3239
-
3240
- # List presentations
3241
- ${ctx.cmd} list
3242
- ${ctx.cmd} list --format json
3231
+ ${cmd2} list # List all presentations
3232
+ ${cmd2} get <slug> # Get details
3233
+ ${cmd2} export <slug> -o p.zip # Export to ZIP
3234
+ ${cmd2} delete <slug> # Delete
3235
+ \`\`\`
3243
3236
 
3244
- # Get presentation details
3245
- ${ctx.cmd} get <id-or-slug>
3237
+ ---
3246
3238
 
3247
- # Export to ZIP
3248
- ${ctx.cmd} export <id-or-slug> -o presentation.zip
3239
+ ## 2. Video Asset Generation
3249
3240
 
3250
- # Import presentation
3251
- ${ctx.cmd} import ./presentation.zip
3241
+ Generate voiceovers, music, and find stock media for video production. Works with Remotion or any video framework.
3252
3242
 
3253
- # Manage branding
3254
- ${ctx.cmd} branding list
3255
- ${ctx.cmd} branding extract https://company.com
3243
+ ### Generate All Assets at Once
3256
3244
 
3257
- # Install/manage this skill
3258
- ${ctx.cmd} skill install
3259
- ${ctx.cmd} skill show
3260
- \`\`\``
3261
- };
3245
+ \`\`\`bash
3246
+ cat <<'EOF' | ${cmd2} video create --output ./public
3247
+ {
3248
+ "scenes": [
3249
+ {
3250
+ "name": "Hook",
3251
+ "script": "Watch how we transformed complex workflows into a single click.",
3252
+ "imageQuery": "modern dashboard dark theme",
3253
+ "videoQuery": "abstract tech particles"
3254
+ },
3255
+ {
3256
+ "name": "Demo",
3257
+ "script": "Our AI analyzes data in real-time, surfacing insights that matter.",
3258
+ "imageQuery": "data visualization charts"
3259
+ },
3260
+ {
3261
+ "name": "CTA",
3262
+ "script": "Start your free trial today.",
3263
+ "imageQuery": "call to action button"
3264
+ }
3265
+ ],
3266
+ "voice": "Kore",
3267
+ "voiceSettings": {
3268
+ "speed": 0.95,
3269
+ "stability": 0.4,
3270
+ "style": 0.6
3271
+ },
3272
+ "musicPrompt": "upbeat corporate, positive energy, modern synth"
3273
+ }
3274
+ EOF
3275
+ \`\`\`
3262
3276
 
3263
- // src/commands/skill/sections/examples.ts
3264
- var examples = {
3265
- title: "Examples",
3266
- render: (ctx) => `## Examples
3277
+ **Output:** Per-scene voiceovers, background music, stock images/videos, and \`video-manifest.json\` with timing data.
3267
3278
 
3268
- ### Present a Codebase Feature
3279
+ ### Initialize Remotion Project
3269
3280
 
3270
3281
  \`\`\`bash
3271
- # Read the relevant files and create presentation
3272
- cat src/lib/auth.ts src/lib/session.ts | ${ctx.cmd} create "Authentication System" \\
3273
- --slides 8 --tone educational --audience "New developers" \\
3274
- --goal train
3282
+ ${cmd2} video init my-video # 16:9 landscape
3283
+ ${cmd2} video init my-video --type tiktok # 9:16 vertical
3275
3284
  \`\`\`
3276
3285
 
3277
- ### Technical Documentation with Diagrams
3286
+ ### Embed Thumbnail
3278
3287
 
3279
3288
  \`\`\`bash
3280
- ${ctx.cmd} create "API Reference" \\
3281
- --file ./docs/api.md \\
3282
- --file ./diagrams/architecture.png \\
3283
- --mode best --amount detailed \\
3284
- --goal inform
3289
+ ${cmd2} video thumbnail video.mp4 --frame 60
3285
3290
  \`\`\`
3286
3291
 
3287
- ### Quick Project Overview
3292
+ ---
3288
3293
 
3289
- \`\`\`bash
3290
- cat README.md package.json | ${ctx.cmd} create "Project Introduction" \\
3291
- -m instant --slides 5
3292
- \`\`\`
3294
+ ## 3. Text-to-Speech (TTS)
3293
3295
 
3294
- ### Sales Deck from Existing Presentation
3296
+ Convert text to natural speech with multiple providers and voices.
3295
3297
 
3296
3298
  \`\`\`bash
3297
- ${ctx.cmd} create "Product Demo" \\
3298
- --file ./existing-deck.pptx \\
3299
- --goal persuade \\
3300
- --audience "Enterprise buyers" \\
3301
- --tone professional
3299
+ # Basic usage
3300
+ ${cmd2} tts generate -t "Hello world" -o output.mp3
3301
+
3302
+ # With voice selection
3303
+ ${cmd2} tts generate -t "Welcome to the demo" -v Rachel -o welcome.mp3
3304
+
3305
+ # With provider and settings
3306
+ ${cmd2} tts generate \\
3307
+ -t "Professional narration" \\
3308
+ -v Kore \\
3309
+ -p gemini \\
3310
+ -s 0.9 \\
3311
+ -o narration.mp3
3312
+
3313
+ # List available voices
3314
+ ${cmd2} tts voices
3315
+ ${cmd2} tts voices --provider elevenlabs
3302
3316
  \`\`\`
3303
3317
 
3304
- ### Research Presentation
3318
+ **Providers:** \`gemini\`, \`elevenlabs\`, \`openai\`
3319
+ **Popular voices:** \`Kore\`, \`Puck\`, \`Rachel\`, \`alloy\`
3320
+ **Speed range:** 0.25 - 4.0 (default: 1.0)
3321
+
3322
+ ---
3323
+
3324
+ ## 4. Music Generation
3325
+
3326
+ Generate AI music from text descriptions.
3305
3327
 
3306
3328
  \`\`\`bash
3307
- ${ctx.cmd} create "Market Analysis" \\
3308
- --file ./research.pdf \\
3309
- --sources https://report.com/industry.pdf \\
3310
- --tone formal --audience "Executive team" \\
3311
- --goal report
3312
- \`\`\``
3313
- };
3329
+ # Generate music
3330
+ ${cmd2} music generate -p "upbeat corporate, modern synth" --duration 30
3314
3331
 
3315
- // src/commands/skill/sections/output.ts
3316
- var output = {
3317
- title: "Output",
3318
- render: (ctx) => `## Output
3332
+ # Save to file
3333
+ ${cmd2} music generate -p "calm ambient background" -o music.mp3
3319
3334
 
3320
- Successful creation returns:
3335
+ # Check generation status (for async operations)
3336
+ ${cmd2} music status <request-id>
3321
3337
  \`\`\`
3322
- \u2713 Presentation created successfully
3323
3338
 
3324
- Title: Authentication System
3325
- Slides: 8
3326
- Generated in: 45s \xB7 12,500 tokens
3339
+ **Duration:** 3-30 seconds
3340
+ **Providers:** \`elevenlabs\`, \`suno\`
3327
3341
 
3328
- Open: ${ctx.url}/en/view/presentations/auth-system-v1-abc123
3329
- \`\`\`
3342
+ ---
3343
+
3344
+ ## 5. Image Search
3345
+
3346
+ Search for stock images from multiple sources.
3330
3347
 
3331
- For scripting, use JSON output:
3332
3348
  \`\`\`bash
3333
- URL=$(${ctx.cmd} create "Demo" --context "..." -o json | jq -r '.viewUrl')
3334
- \`\`\``
3335
- };
3349
+ # Basic search
3350
+ ${cmd2} image search -q "mountain landscape"
3336
3351
 
3337
- // src/commands/skill/sections/best-practices.ts
3338
- var bestPractices = {
3339
- title: "Best Practices",
3340
- render: (_ctx) => `## Best Practices
3341
-
3342
- 1. **Provide rich context** - More context = better slides. Include code, docs, data.
3343
- 2. **Use file uploads for binary content** - PDFs, images, PPTX files need \`--file\`.
3344
- 3. **Specify a goal** - Helps tailor the presentation structure and messaging.
3345
- 4. **Use appropriate mode** - \`instant\` for quick drafts, \`best\` for important presentations.
3346
- 5. **Specify audience** - Helps tailor complexity and terminology.
3347
- 6. **Combine sources** - Pipe multiple files for comprehensive presentations.`
3348
- };
3352
+ # With options
3353
+ ${cmd2} image search -q "business team meeting" \\
3354
+ --max-results 20 \\
3355
+ --size large \\
3356
+ --format json
3349
3357
 
3350
- // src/commands/skill/sections/file-types.ts
3351
- var fileTypes = {
3352
- title: "Supported File Types",
3353
- render: (_ctx) => `## Supported File Types
3358
+ # Download for video project
3359
+ ${cmd2} image search -q "tech abstract" -n 5 --format json > images.json
3360
+ \`\`\`
3354
3361
 
3355
- - **Documents**: PDF, DOCX, XLSX, PPTX
3356
- - **Images**: JPEG, PNG, GIF, WebP
3357
- - **Text**: Markdown, TXT, CSV, JSON`
3358
- };
3362
+ **Options:**
3363
+ - \`--max-results <n>\` - Number of results (default: 10)
3364
+ - \`--size <small|medium|large|any>\` - Image size
3365
+ - \`--safe-search / --no-safe-search\` - Filter adult content
3359
3366
 
3360
- // src/commands/skill/sections/troubleshooting.ts
3361
- var troubleshooting = {
3362
- title: "Troubleshooting",
3363
- render: (ctx) => `## Troubleshooting
3367
+ ---
3368
+
3369
+ ## 6. Video Search
3370
+
3371
+ Search for stock video clips.
3364
3372
 
3365
3373
  \`\`\`bash
3366
- # Check if authenticated
3367
- ${ctx.cmd} whoami
3374
+ # Basic search
3375
+ ${cmd2} video search "ocean waves"
3376
+
3377
+ # With filters
3378
+ ${cmd2} video search "city timelapse" \\
3379
+ --max-results 5 \\
3380
+ --orientation landscape \\
3381
+ --license free
3382
+ \`\`\`
3368
3383
 
3369
- # Re-authenticate if needed
3370
- ${ctx.cmd} login
3384
+ **Options:**
3385
+ - \`--orientation <landscape|portrait|square|any>\`
3386
+ - \`--license <free|premium|any>\`
3371
3387
 
3372
- # Debug mode
3373
- ${ctx.cmd} create "Test" --context "test" --debug
3374
- \`\`\``
3375
- };
3388
+ ---
3376
3389
 
3377
- // src/commands/skill/sections/video-creation.ts
3378
- var videoCreation = {
3379
- title: "Video Creation",
3380
- render: (ctx) => `## Video Creation
3390
+ ## 7. Audio Mixing
3381
3391
 
3382
- ### How to Create a Perfect Video
3392
+ Mix video with voiceover and background music.
3383
3393
 
3384
- Videos must feel like premium tech launches (Stripe, Apple, Linear) - **Kinetic Composition**, not slideshows.
3394
+ \`\`\`bash
3395
+ # Mix audio tracks
3396
+ ${cmd2} mix create \\
3397
+ --video input.mp4 \\
3398
+ --voice voiceover.mp3 \\
3399
+ --music background.mp3 \\
3400
+ --music-volume 30 \\
3401
+ --voice-volume 100 \\
3402
+ -o final.mp4
3403
+
3404
+ # Check mixing status
3405
+ ${cmd2} mix status <request-id>
3406
+ \`\`\`
3385
3407
 
3386
- **Core Philosophy:** "Nothing sits still. Everything is physics-based. Every pixel breathes."
3408
+ Music automatically loops to match video duration.
3387
3409
 
3388
- ### Required Reading (The Motion Bible)
3410
+ ---
3389
3411
 
3390
- | Rule | Why This Matters |
3391
- |------|------------------|
3392
- | [video-creation-guide.md](rules/video/video-creation-guide.md) | **The Motion Bible.** Camera rigs, 2.5D entrances, cursor physics, typography, backgrounds |
3393
- | [failures.md](rules/video/failures.md) | **Avoid rejection.** 7 common mistakes that make videos look like slideshows |
3394
- | [project-based.md](rules/video/project-based.md) | **Copy real components.** Eject actual UI code, strip logic, animate pixel-perfect |
3395
- | [parameterization.md](rules/video/parameterization.md) | **Saves debugging.** Never hardcode frame numbers |
3396
- | [layers.md](rules/video/layers.md) | **Prevents z-index bugs.** Background orbs \u2192 Vignette \u2192 CameraRig \u2192 Content |
3397
- | [social-media.md](rules/video/social-media.md) | **Platform specs.** Resolution, aspect ratio, duration per platform |
3412
+ ## 8. Branding
3398
3413
 
3399
- **Required:** \`npx skills add https://github.com/remotion-dev/skills --skill remotion-best-practices\`
3414
+ Manage brand profiles for consistent styling.
3400
3415
 
3401
- ### Workflow
3416
+ \`\`\`bash
3417
+ # List saved brands
3418
+ ${cmd2} branding list
3402
3419
 
3403
- #### Phase 1: Discovery
3420
+ # Extract branding from website
3421
+ ${cmd2} branding extract https://company.com
3404
3422
 
3405
- Explore the project thoroughly - assets, components, branding, what makes this product/topic unique.
3423
+ # Get brand details
3424
+ ${cmd2} branding get <brand-id>
3406
3425
 
3407
- #### Phase 2: Video Brief
3426
+ # Set default brand
3427
+ ${cmd2} branding set-default <brand-id>
3408
3428
 
3409
- Present a brief outline (scenes, duration, assets found) and get user approval before production.
3429
+ # Use in presentations
3430
+ ${cmd2} create "Topic" --brand my-company
3431
+ \`\`\`
3410
3432
 
3411
- #### Phase 3: Production
3433
+ ---
3412
3434
 
3413
- 1. **Read video-creation-guide.md + project-based.md + remotion-best-practices**
3414
- 2. **Generate audio** - \`${ctx.cmd} video create\` with scenes JSON
3415
- 3. **Scaffold OUTSIDE project** - \`cd .. && ${ctx.cmd} video init my-video\`
3416
- 4. **Copy assets** - Logo, fonts, colors from project into video project
3417
- 5. **Implement kinetically** - Camera rig, 2.5D entrances, staggered lists, transitions
3418
- 6. **Verify checklist** - Is it kinetic or static?
3435
+ ## 9. Configuration
3419
3436
 
3420
- #### Phase 4: Render
3437
+ \`\`\`bash
3438
+ # Interactive setup
3439
+ ${cmd2} config init
3421
3440
 
3422
- Auto-render when done: \`pnpm exec remotion render FullVideo\`
3441
+ # Show current config
3442
+ ${cmd2} config show
3443
+ ${cmd2} config show --verify # Verify API key
3423
3444
 
3424
- Only open Studio if user asks.
3445
+ # Set values
3446
+ ${cmd2} config set api-key <key>
3447
+ ${cmd2} config set team-id <id>
3425
3448
 
3426
- ### Kinetic Checklist
3449
+ # Clear config
3450
+ ${cmd2} config clear
3427
3451
 
3428
- - [ ] CameraRig wraps entire scene with drift/zoom
3429
- - [ ] Every UI element uses 2.5D rotation entrance (\`perspective + rotateX\`)
3430
- - [ ] Cursor moves in Bezier curves with overshoot
3431
- - [ ] Lists/grids stagger (never appear all at once)
3432
- - [ ] Text uses masked reveal or keyword animation
3433
- - [ ] Background has moving orbs + vignette + noise
3434
- - [ ] Something is moving on EVERY frame
3435
- - [ ] No static resting states longer than 30 frames
3452
+ # Show config file location
3453
+ ${cmd2} config path
3454
+ \`\`\`
3436
3455
 
3437
- ### Asset Generation
3456
+ ---
3438
3457
 
3439
- \`\`\`bash
3440
- cat <<EOF | ${ctx.cmd} video create --output ./public
3441
- {
3442
- "scenes": [
3443
- { "name": "Hook", "script": "..." },
3444
- { "name": "Demo", "script": "..." },
3445
- { "name": "CTA", "script": "..." }
3446
- ],
3447
- "voice": "Kore",
3448
- "musicPrompt": "upbeat corporate"
3449
- }
3450
- EOF
3451
- \`\`\``
3452
- };
3458
+ ## Output Formats
3453
3459
 
3454
- // src/commands/skill/generate-content.ts
3455
- var DEFAULT_SECTIONS = [
3456
- frontmatter,
3457
- header3,
3458
- prerequisites,
3459
- workflow,
3460
- createCommand2,
3461
- createOptions,
3462
- otherCommands,
3463
- examples,
3464
- output,
3465
- videoCreation,
3466
- bestPractices,
3467
- fileTypes,
3468
- troubleshooting
3469
- ];
3470
- function createSkillContext(brand2) {
3471
- return {
3472
- cmd: brand2.name,
3473
- pkg: brand2.packageName,
3474
- url: brand2.apiUrl,
3475
- name: brand2.displayName
3476
- };
3477
- }
3478
- function generateSkillContent(brand2, sections = DEFAULT_SECTIONS) {
3479
- const ctx = createSkillContext(brand2);
3480
- return sections.map((section) => section.render(ctx)).join("\n\n");
3481
- }
3460
+ All commands support multiple output formats:
3482
3461
 
3483
- // src/commands/skill/installer.ts
3484
- import { mkdirSync, writeFileSync, existsSync as existsSync2, rmSync } from "fs";
3485
- import { join, resolve as resolve4, relative } from "path";
3486
- import { homedir } from "os";
3462
+ \`\`\`bash
3463
+ ${cmd2} <command> --format human # Default, colored terminal output
3464
+ ${cmd2} <command> --format json # Machine-readable JSON
3465
+ ${cmd2} <command> --format quiet # Minimal output, errors only
3466
+ \`\`\`
3487
3467
 
3488
- // src/commands/skill/editors.ts
3489
- var SUPPORTED_EDITORS = [
3490
- { name: "Claude Code", dir: ".claude" },
3491
- { name: "Cursor", dir: ".cursor" },
3492
- { name: "Codex", dir: ".codex" },
3493
- { name: "OpenCode", dir: ".opencode" },
3494
- { name: "Windsurf", dir: ".windsurf" },
3495
- { name: "Agent", dir: ".agent" }
3496
- ];
3468
+ ---
3497
3469
 
3498
- // src/commands/skill/rules/video/content.ts
3499
- var VIDEO_RULE_CONTENTS = [
3500
- {
3501
- filename: "video-creation-guide.md",
3502
- content: `# Video Creation Guide
3470
+ ## Environment Variables
3503
3471
 
3504
- > **ALSO READ:**
3505
- > - [project-based.md](project-based.md) - How to extract and animate real project components pixel-perfect
3506
- > - If installed, check \`remotion-best-practices\` skill for Remotion-specific patterns
3472
+ \`\`\`bash
3473
+ ${envPrefix}_API_KEY # API key for authentication
3474
+ ${envPrefix}_API_URL # Custom API URL (optional)
3475
+ \`\`\`
3507
3476
 
3508
3477
  ---
3509
3478
 
3510
- # The "Kinetic SaaS" Motion Design System
3511
-
3512
- **Objective:** Replicate the high-energy, fluid feel of premium tech product videos (Stripe, Apple, Linear, Affable.ai).
3479
+ ## Specialized Skills
3513
3480
 
3514
- **Core Philosophy:** "Nothing sits still. Everything is physics-based. Every pixel breathes."
3481
+ For detailed workflows, load these skills:
3515
3482
 
3516
- This is the difference between **Static Composition** (slideshows) and **Kinetic Composition** (motion graphics). You must rebuild UI as individual animated layers and film with a virtual camera.
3483
+ - **\`${name}-video\`** - Detailed video creation workflow, Remotion integration, composition rules, R3F/Three.js patterns
3484
+ - **\`${name}-presentation\`** - Detailed presentation creation, styling options, export formats
3517
3485
 
3518
3486
  ---
3519
3487
 
3520
- ## 1. The Global Camera Rig (The "Anti-Static" Layer)
3521
-
3522
- Even when reading text, the screen is slowly zooming or panning.
3488
+ ## Common Workflows
3523
3489
 
3524
- ### Virtual Camera
3490
+ ### Video Production Pipeline
3525
3491
 
3526
- Every scene must be wrapped in a \`CameraRig\` component:
3492
+ \`\`\`bash
3493
+ # 1. Initialize project
3494
+ ${cmd2} video init product-demo
3527
3495
 
3528
- \`\`\`tsx
3529
- const CameraRig: React.FC<{ children: React.ReactNode }> = ({ children }) => {
3530
- const frame = useCurrentFrame();
3531
- const { durationInFrames } = useVideoConfig();
3496
+ # 2. Generate assets
3497
+ cat scenes.json | ${cmd2} video create -o product-demo/public
3532
3498
 
3533
- // The "Drift" - constant subtle movement
3534
- const scale = interpolate(frame, [0, durationInFrames], [1.0, 1.05]);
3535
- const rotation = interpolate(frame, [0, durationInFrames], [0, 0.5]);
3499
+ # 3. Implement in Remotion (see ${name}-video skill)
3536
3500
 
3537
- return (
3538
- <AbsoluteFill style={{
3539
- transform: \`scale(\${scale}) rotate(\${rotation}deg)\`,
3540
- }}>
3541
- {children}
3542
- </AbsoluteFill>
3543
- );
3544
- };
3501
+ # 4. Render and add thumbnail
3502
+ cd product-demo
3503
+ pnpm exec remotion render FullVideo
3504
+ ${cmd2} video thumbnail out/FullVideo.mp4 --frame 60
3545
3505
  \`\`\`
3546
3506
 
3547
- ### Zoom-Through Transitions
3507
+ ### Presentation from Research
3508
+
3509
+ \`\`\`bash
3510
+ # Gather content from multiple sources
3511
+ ${cmd2} create "Market Analysis" \\
3512
+ --file research.pdf \\
3513
+ --sources https://industry-report.com \\
3514
+ --context "Focus on Q4 trends" \\
3515
+ --slides 15 \\
3516
+ --tone professional \\
3517
+ --brand company \\
3518
+ --open
3519
+ \`\`\`
3548
3520
 
3549
- Transitions are NOT just fades - they are camera movements:
3521
+ ### Batch Image Collection
3550
3522
 
3551
- \`\`\`tsx
3552
- // Camera scales rapidly into an element (logo, dot) until it fills screen
3553
- const zoomThrough = interpolate(frame, [TRANSITION_START, TRANSITION_END], [1, 50]);
3554
- // The element becomes the background for the next scene
3523
+ \`\`\`bash
3524
+ # Get images for video scenes
3525
+ for query in "hero shot" "team photo" "product showcase"; do
3526
+ ${cmd2} image search -q "$query" -n 3 --format json
3527
+ done
3555
3528
  \`\`\`
3556
3529
 
3557
3530
  ---
3558
3531
 
3559
- ## 2. UI & Mockup Animation Rules (Rebuilding Reality)
3532
+ ## Troubleshooting
3533
+
3534
+ **Authentication issues:**
3535
+ \`\`\`bash
3536
+ ${cmd2} whoami # Check current user
3537
+ ${cmd2} logout && ${cmd2} login # Re-authenticate
3538
+ \`\`\`
3539
+
3540
+ **Check API status:**
3541
+ \`\`\`bash
3542
+ ${cmd2} config show --verify
3543
+ \`\`\`
3560
3544
 
3561
- UI doesn't just fade in. It pops up with weight.
3545
+ **Debug mode:**
3546
+ \`\`\`bash
3547
+ ${cmd2} <command> --debug
3548
+ \`\`\`
3562
3549
 
3563
- ### Priority: Use Real Project Components
3550
+ ---
3564
3551
 
3565
- **If you're in a project folder, ALWAYS check for actual components first:**
3552
+ ## Help
3566
3553
 
3567
3554
  \`\`\`bash
3568
- # Before building ANY UI, explore the project
3569
- ls src/components/
3570
- ls src/app/
3555
+ ${cmd2} --help # General help
3556
+ ${cmd2} <command> --help # Command-specific help
3557
+ ${cmd2} --version # Version info
3571
3558
  \`\`\`
3559
+ `;
3560
+ }
3572
3561
 
3573
- **When project components exist:**
3574
- 1. **Copy the actual component** into your Remotion project
3575
- 2. **Strip logic** (remove useState, API calls, event handlers)
3576
- 3. **Keep visuals identical** (same styles, colors, spacing)
3577
- 4. **Add animation props** (progress, isHovered, etc.)
3578
- 5. **Apply kinetic animation** using the rules below
3562
+ // src/commands/skill/rules/video/content.ts
3563
+ var VIDEO_RULE_CONTENTS = [
3564
+ {
3565
+ filename: "video-creation-guide.md",
3566
+ content: `# Video Creation Guide
3579
3567
 
3580
- See [project-based.md](project-based.md) for the full component extraction process.
3568
+ ### Related Skills
3581
3569
 
3582
- **This is the priority order:**
3583
- 1. \u2705 Pixel-perfect copy of real project components (animated)
3584
- 2. \u26A0\uFE0F Recreate UI from project's design system (colors, fonts, spacing)
3585
- 3. \u274C Generic mockups or stock images (ONLY for non-UI scenes like hooks)
3570
+ Use these installed skills for implementation details:
3571
+ - \`remotion-best-practices\` \u2014 Remotion patterns and API
3572
+ - \`threejs-*\` skills \u2014 for R3F/WebGL (particles, 3D elements)
3586
3573
 
3587
- ### The "Pop-Up" Entrance (2.5D Rotation)
3574
+ ---
3588
3575
 
3589
- \`\`\`tsx
3590
- // Heavy spring for weighty feel
3591
- const progress = spring({
3592
- frame,
3593
- fps,
3594
- config: { mass: 2, damping: 20, stiffness: 100 },
3595
- });
3576
+ ## Core Rules
3596
3577
 
3597
- // Start tilted back, spring to flat
3598
- const rotateX = interpolate(progress, [0, 1], [20, 0]);
3599
- const y = interpolate(progress, [0, 1], [100, 0]);
3600
- const scale = interpolate(progress, [0, 1], [0.8, 1.0]);
3578
+ Your task is not "making slideshows" \u2014 you are **simulating a real interface** that obeys cinematic physics.
3601
3579
 
3602
- <div style={{
3603
- transform: \`perspective(1000px) rotateX(\${rotateX}deg) translateY(\${y}px) scale(\${scale})\`,
3604
- }}>
3605
- {uiComponent}
3606
- </div>
3607
- \`\`\`
3580
+ ### Hard Constraints
3581
+
3582
+ 1. **No scene > 8 seconds** without cut or major action
3583
+ 2. **No static pixels** \u2014 everything breathes, drifts, pulses
3584
+ 3. **No linear interpolation** \u2014 use \`spring()\` physics
3585
+ 4. **Scene overlap 15-20 frames** \u2014 no hard cuts
3586
+ 5. **60 FPS mandatory** \u2014 30fps looks choppy
3587
+ 6. **No screenshots for UI** \u2014 rebuild in React/CSS
3588
+
3589
+ ---
3608
3590
 
3609
- ### Cursor Simulation
3591
+ ## Code Organization
3610
3592
 
3611
- **Movement:** Never linear. Cursors move in **Bezier Curves** with an arc.
3593
+ - Create separate files: \`Button.tsx\`, \`Window.tsx\`, \`Cursor.tsx\`
3594
+ - Use Zod schemas for props validation
3595
+ - Extract animation configs to constants
3612
3596
 
3613
3597
  \`\`\`tsx
3614
- // 1. Spring progress from start to end
3615
- const progress = spring({
3616
- frame: frame - startFrame,
3617
- fps,
3618
- config: { damping: 20, stiffness: 80 }, // Slower, deliberate
3619
- });
3598
+ import { spring, interpolate, useCurrentFrame, useVideoConfig } from 'remotion';
3620
3599
 
3621
- // 2. Linear interpolation for X and Y
3622
- const linearX = interpolate(progress, [0, 1], [start.x, end.x]);
3623
- const linearY = interpolate(progress, [0, 1], [start.y, end.y]);
3600
+ // ALWAYS use spring for element entrances
3601
+ // NEVER use magic numbers
3602
+ \`\`\`
3624
3603
 
3625
- // 3. THE ARC: Parabola that peaks mid-travel (this is the secret sauce)
3626
- const arcHeight = 100; // How much the cursor "loops"
3627
- const arcOffset = Math.sin(progress * Math.PI) * arcHeight;
3604
+ ---
3628
3605
 
3629
- // 4. Apply arc to Y position
3630
- const cursorY = linearY - arcOffset;
3606
+ ## Aesthetics (Linear/Stripe Style)
3607
+
3608
+ \`\`\`css
3609
+ /* Shadows - soft, expensive */
3610
+ box-shadow: 0 20px 50px -12px rgba(0,0,0,0.5);
3611
+
3612
+ /* Borders - thin, barely visible */
3613
+ border: 1px solid rgba(255,255,255,0.1);
3631
3614
  \`\`\`
3632
3615
 
3633
- **Click Interaction:**
3634
- \`\`\`tsx
3635
- // On click:
3636
- // 1. Cursor scales down
3637
- const cursorScale = isClicking ? 0.8 : 1;
3616
+ - Fonts: Inter or SF Pro
3617
+ - Never pure \`#000000\` \u2014 use \`#050505\`
3618
+ - Never pure \`#FFFFFF\` \u2014 use \`#F0F0F0\`
3638
3619
 
3639
- // 2. Button squishes
3640
- const buttonScaleX = isClicking ? 1.05 : 1;
3641
- const buttonScaleY = isClicking ? 0.95 : 1;
3620
+ ---
3621
+
3622
+ ## Self-Check Before Render
3623
+
3624
+ - [ ] Camera rig wraps entire scene with drift/zoom
3625
+ - [ ] Every UI element uses 2.5D rotation entrance
3626
+ - [ ] Cursor moves in curves with overshoot
3627
+ - [ ] Lists/grids stagger (never appear all at once)
3628
+ - [ ] Background has moving orbs + vignette + noise
3629
+ - [ ] Something is moving on EVERY frame
3630
+ - [ ] Scene transitions overlap (no hard cuts)
3631
+
3632
+ **If your video looks like PowerPoint with voiceover \u2014 START OVER.**
3633
+ `
3634
+ },
3635
+ {
3636
+ filename: "animation-physics.md",
3637
+ content: `# Animation Physics
3638
+
3639
+ ## Spring Configurations
3642
3640
 
3643
- // 3. Release both with spring
3641
+ ### Heavy UI (Modals, Sidebars)
3642
+ \`\`\`tsx
3643
+ config: { mass: 1, stiffness: 100, damping: 15 }
3644
3644
  \`\`\`
3645
3645
 
3646
- **Standard Cursor SVG:**
3646
+ ### Light UI (Tooltips, Badges)
3647
3647
  \`\`\`tsx
3648
- // Mac-style cursor (use as reference, customize as needed)
3649
- <svg width="32" height="32" viewBox="0 0 32 32" fill="none">
3650
- <path
3651
- d="M4 4L11.5 26L16 17.5L25 25L27 23L18.5 15L28 11.5L4 4Z"
3652
- fill="black"
3653
- stroke="white"
3654
- strokeWidth="2"
3655
- />
3656
- </svg>
3648
+ config: { mass: 0.6, stiffness: 180, damping: 12 }
3657
3649
  \`\`\`
3658
3650
 
3659
- ### Staggered Lists & Grids
3651
+ ### Standard (Snappy)
3652
+ \`\`\`tsx
3653
+ config: { mass: 1, damping: 15, stiffness: 120 }
3654
+ \`\`\`
3655
+
3656
+ ---
3660
3657
 
3661
- **Rule:** NEVER show a list or grid all at once.
3658
+ ## Staggering
3659
+
3660
+ **NEVER show a list all at once.**
3662
3661
 
3663
3662
  \`\`\`tsx
3664
- const STAGGER_FRAMES = 4; // ~0.05s at 60fps
3663
+ const STAGGER_FRAMES = 3; // 3-5 frames between items
3665
3664
 
3666
3665
  {items.map((item, i) => {
3667
3666
  const delay = i * STAGGER_FRAMES;
@@ -3684,904 +3683,1064 @@ const STAGGER_FRAMES = 4; // ~0.05s at 60fps
3684
3683
 
3685
3684
  ---
3686
3685
 
3687
- ## 3. Kinetic Typography (Text that Hits)
3686
+ ## Cursor Movement
3688
3687
 
3689
- Text doesn't fade. It slams in, changes fill, or slides up.
3688
+ **Cursor NEVER moves in straight lines.**
3690
3689
 
3691
- ### Pattern A: The "Masked Reveal"
3690
+ \`\`\`tsx
3691
+ const progress = spring({
3692
+ frame: frame - startFrame,
3693
+ fps,
3694
+ config: { damping: 20, stiffness: 80 },
3695
+ });
3692
3696
 
3693
- Text rises from a floor:
3697
+ const linearX = interpolate(progress, [0, 1], [start.x, end.x]);
3698
+ const linearY = interpolate(progress, [0, 1], [start.y, end.y]);
3694
3699
 
3695
- \`\`\`tsx
3696
- <div style={{ overflow: 'hidden', height: 80 }}>
3697
- <h1 style={{
3698
- transform: \`translateY(\${interpolate(progress, [0, 1], [100, 0])}%)\`,
3699
- }}>
3700
- INTRODUCING
3701
- </h1>
3702
- </div>
3700
+ // THE ARC: Parabola that peaks mid-travel
3701
+ const arcHeight = 100;
3702
+ const arcOffset = Math.sin(progress * Math.PI) * arcHeight;
3703
+ const cursorY = linearY - arcOffset;
3703
3704
  \`\`\`
3704
3705
 
3705
- ### Pattern B: Variable Weight (Animate Keywords)
3706
+ ---
3706
3707
 
3707
- Don't animate the whole sentence. Animate **keywords**:
3708
+ ## Click Interaction
3708
3709
 
3709
3710
  \`\`\`tsx
3710
- // "Teams waste HOURS" - only "HOURS" animates
3711
- <p>
3712
- Teams waste{' '}
3713
- <span style={{
3714
- transform: \`scale(\${interpolate(keywordProgress, [0, 1], [1, 1.2])})\`,
3715
- color: interpolateColors(keywordProgress, [0, 1], ['#F0F0F0', '#FF6B6B']),
3716
- textShadow: \`0 0 \${glowProgress * 20}px rgba(255,107,107,0.5)\`,
3717
- }}>
3718
- HOURS
3719
- </span>
3720
- </p>
3711
+ // On click:
3712
+ const cursorScale = isClicking ? 0.95 : 1;
3713
+ const buttonScaleX = isClicking ? 1.02 : 1;
3714
+ const buttonScaleY = isClicking ? 0.95 : 1;
3715
+ // Release both with spring
3721
3716
  \`\`\`
3722
3717
 
3723
- ### Pattern C: The "Glitch/Tech" Accent
3718
+ ---
3719
+
3720
+ ## Timing Reference
3721
+
3722
+ | Action | Frames (60fps) |
3723
+ |--------|----------------|
3724
+ | Element entrance | 15-20 |
3725
+ | Stagger gap | 3-5 |
3726
+ | Hold on key info | 45-60 |
3727
+ | Scene transition | 20-30 |
3728
+ | Fast interaction | 15-20 |
3729
+ `
3730
+ },
3731
+ {
3732
+ filename: "scene-structure.md",
3733
+ content: `# Scene Structure
3724
3734
 
3725
- Chromatic aberration on impact:
3735
+ ## SceneWrapper
3726
3736
 
3727
3737
  \`\`\`tsx
3728
- // Duplicate text, offset by 2px with color channels, flash for 2 frames
3729
- {isImpactFrame && (
3730
- <>
3731
- <span style={{ position: 'absolute', left: -2, color: '#FF0000', opacity: 0.5 }}>TEXT</span>
3732
- <span style={{ position: 'absolute', left: 2, color: '#0000FF', opacity: 0.5 }}>TEXT</span>
3733
- </>
3734
- )}
3738
+ <SceneWrapper
3739
+ durationInFrames={300}
3740
+ transitionType="slideLeft"
3741
+ cameraMotion="panRight"
3742
+ >
3743
+ <FeatureLayer />
3744
+ <CursorLayer />
3745
+ <ParticleLayer />
3746
+ </SceneWrapper>
3735
3747
  \`\`\`
3736
3748
 
3737
- ### Text Colors
3749
+ ---
3750
+
3751
+ ## Layer Structure (Z-Index)
3738
3752
 
3739
- Never pure white (\`#FFF\`). Use \`#F0F0F0\` with subtle gradient or shadow for depth.
3753
+ | Layer | Z-Index |
3754
+ |-------|---------|
3755
+ | Background orbs | 0 |
3756
+ | Vignette | 1 |
3757
+ | UI Base | 10 |
3758
+ | UI Elements | 20 |
3759
+ | Overlays | 30 |
3760
+ | Text/Captions | 40 |
3761
+ | Cursor | 50 |
3740
3762
 
3741
3763
  ---
3742
3764
 
3743
- ## 4. Atmosphere & Backgrounds (The "Deep Space")
3765
+ ## Case Study: SaaS Task Tracker
3744
3766
 
3745
- Background is never a solid color. It's deep black with floating colored orbs.
3767
+ ### Scene 1: "The Hook" (~5s)
3746
3768
 
3747
- ### The "Orb" System
3769
+ 1. Dark background (\`#0B0C10\`), grid drifting
3770
+ 2. Scattered circles magnetically attract \u2192 morph into logo
3771
+ 3. Logo expands \u2192 becomes sidebar navigation
3748
3772
 
3749
- \`\`\`tsx
3750
- const MovingBackground: React.FC = () => {
3751
- const frame = useCurrentFrame();
3773
+ ### Scene 2: "Micro-Interaction" (~6s)
3752
3774
 
3753
- // Orbs move in figure-8 or circular patterns
3754
- const orb1X = Math.sin(frame / 60) * 200;
3755
- const orb1Y = Math.cos(frame / 80) * 100;
3756
- const orb2X = Math.sin(frame / 70 + Math.PI) * 150;
3757
- const orb2Y = Math.cos(frame / 90 + Math.PI) * 120;
3775
+ 1. Modal "Create Issue" appears
3776
+ 2. Text types character by character (non-uniform speed)
3777
+ 3. \`CMD + K\` hint glows, keys animate
3778
+ 4. Cursor flies to "Save" in arc, slows on approach
3758
3779
 
3759
- return (
3760
- <AbsoluteFill style={{ backgroundColor: '#0a0a0a' }}>
3761
- {/* Orb 1 - Teal */}
3762
- <div style={{
3763
- position: 'absolute',
3764
- width: 600,
3765
- height: 600,
3766
- borderRadius: '50%',
3767
- background: 'radial-gradient(circle, rgba(20,184,166,0.3) 0%, transparent 70%)',
3768
- filter: 'blur(100px)',
3769
- transform: \`translate(\${orb1X}px, \${orb1Y}px)\`,
3770
- left: '20%',
3771
- top: '30%',
3772
- }} />
3773
-
3774
- {/* Orb 2 - Purple */}
3775
- <div style={{
3776
- position: 'absolute',
3777
- width: 500,
3778
- height: 500,
3779
- borderRadius: '50%',
3780
- background: 'radial-gradient(circle, rgba(168,85,247,0.3) 0%, transparent 70%)',
3781
- filter: 'blur(100px)',
3782
- transform: \`translate(\${orb2X}px, \${orb2Y}px)\`,
3783
- right: '20%',
3784
- bottom: '20%',
3785
- }} />
3786
- </AbsoluteFill>
3787
- );
3788
- };
3789
- \`\`\`
3780
+ ### Scene 3: "The Connection" (~5s)
3790
3781
 
3791
- ### Vignette & Noise
3782
+ 1. Task card grabbed, scales 1.05, shadow deepens
3783
+ 2. Other cards spread apart
3784
+ 3. **Match Cut:** Zoom into avatar \u2192 color fills screen \u2192 becomes mobile notification background
3792
3785
 
3793
- \`\`\`tsx
3794
- // Noise texture overlay (5% opacity)
3795
- <AbsoluteFill style={{
3796
- backgroundImage: 'url(/noise.png)',
3797
- opacity: 0.05,
3798
- mixBlendMode: 'overlay',
3799
- }} />
3786
+ ---
3800
3787
 
3801
- // Vignette (dark corners)
3802
- <AbsoluteFill style={{
3803
- background: 'radial-gradient(ellipse at center, transparent 40%, rgba(0,0,0,0.8) 100%)',
3804
- }} />
3788
+ ## Composition
3789
+
3790
+ \`\`\`tsx
3791
+ <AbsoluteFill>
3792
+ <MovingBackground />
3793
+ <Vignette />
3794
+ <CameraRig>
3795
+ <Sequence from={0} durationInFrames={100}>
3796
+ <Scene1 />
3797
+ </Sequence>
3798
+ <Sequence from={85} durationInFrames={150}> {/* 15 frame overlap! */}
3799
+ <Scene2 />
3800
+ </Sequence>
3801
+ </CameraRig>
3802
+ <Audio src={music} volume={0.3} />
3803
+ </AbsoluteFill>
3805
3804
  \`\`\`
3805
+ `
3806
+ },
3807
+ {
3808
+ filename: "scene-transitions.md",
3809
+ content: `# Scene Transitions
3810
+
3811
+ **No FadeIn/FadeOut.** Only contextual transitions.
3806
3812
 
3807
3813
  ---
3808
3814
 
3809
- ## 5. Physics & Timing Reference
3815
+ ## Types
3810
3816
 
3811
- ### Spring Configs
3817
+ ### 1. Object Persistence
3818
+ Same chart transforms (data, color, scale) while UI changes around it.
3812
3819
 
3813
- | Use Case | Config | Feel |
3814
- |----------|--------|------|
3815
- | Standard (snappy) | \`{ mass: 1, damping: 15, stiffness: 120 }\` | Smooth, professional |
3816
- | Heavy (UI mockups) | \`{ mass: 2, damping: 20, stiffness: 100 }\` | Weighty, premium |
3817
- | Bouncy (attention) | \`{ mass: 1, damping: 10, stiffness: 150 }\` | Playful overshoot |
3820
+ ### 2. Mask Reveal
3821
+ Button expands to screen size via SVG \`clipPath\`.
3818
3822
 
3819
- ### Timing Values
3823
+ ### 3. Speed Ramps
3824
+ Scene A accelerates out, Scene B starts fast then slows.
3820
3825
 
3821
- | Action | Frames | Notes |
3822
- |--------|--------|-------|
3823
- | Element entrance | 15-20 | Spring to rest |
3824
- | Stagger gap | 3-5 | Between list items |
3825
- | Hold on key info | 45-60 | Minimum read time |
3826
- | Scene transition | 20-30 | Zoom-through |
3826
+ ---
3827
3827
 
3828
- ### The Golden Rule
3828
+ ## Match Cut Example
3829
3829
 
3830
- Use \`interpolate(frame)\` to ensure \`scale\` or \`rotation\` is changing on **EVERY frame**. No static resting states.
3830
+ \`\`\`
3831
+ Scene A: Zoom into avatar
3832
+ \u2193
3833
+ Avatar color fills screen
3834
+ \u2193
3835
+ Scene B: That color IS the notification background
3836
+ \`\`\`
3831
3837
 
3832
3838
  ---
3833
3839
 
3834
- ## 6. Micro-Tricks from Premium Videos
3840
+ ## Overlapping Sequences (CRITICAL)
3835
3841
 
3836
- ### The "Match Cut"
3842
+ \`\`\`tsx
3843
+ <Sequence from={0} durationInFrames={100}>
3844
+ <SceneOne />
3845
+ </Sequence>
3846
+ <Sequence from={85} durationInFrames={150}> {/* 15 frames early! */}
3847
+ <SceneTwo />
3848
+ </Sequence>
3849
+ \`\`\`
3850
+
3851
+ ---
3837
3852
 
3838
- Small circle zooms to fill screen, becomes next scene's background:
3853
+ ## TransitionSeries
3839
3854
 
3840
3855
  \`\`\`tsx
3841
- // Circle element scales 50x to wipe to next scene
3842
- const circleScale = interpolate(frame, [MATCH_START, MATCH_END], [1, 50]);
3856
+ import { TransitionSeries, linearTiming } from '@remotion/transitions';
3857
+ import { slide } from '@remotion/transitions/slide';
3858
+
3859
+ <TransitionSeries>
3860
+ <TransitionSeries.Sequence durationInFrames={100}>
3861
+ <SceneOne />
3862
+ </TransitionSeries.Sequence>
3863
+ <TransitionSeries.Transition
3864
+ presentation={slide({ direction: 'from-bottom' })}
3865
+ timing={linearTiming({ durationInFrames: 20 })}
3866
+ />
3867
+ <TransitionSeries.Sequence durationInFrames={150}>
3868
+ <SceneTwo />
3869
+ </TransitionSeries.Sequence>
3870
+ </TransitionSeries>
3843
3871
  \`\`\`
3872
+ `
3873
+ },
3874
+ {
3875
+ filename: "polish-effects.md",
3876
+ content: `# Polish Effects
3844
3877
 
3845
- ### Search Bar Typing
3878
+ ## Reflection (Glass Glint)
3846
3879
 
3847
- Include blinking cursor:
3880
+ Diagonal gradient sweeps every 5 seconds.
3848
3881
 
3849
3882
  \`\`\`tsx
3850
- const Typewriter: React.FC<{ text: string }> = ({ text }) => {
3851
- const frame = useCurrentFrame();
3852
- const charIndex = Math.floor(frame / 3); // 3 frames per char
3853
- const showCursor = Math.floor(frame / 15) % 2 === 0; // Blink every 15 frames
3883
+ const cycleFrame = frame % 300;
3884
+ const sweepProgress = interpolate(cycleFrame, [0, 60], [-100, 200], {
3885
+ extrapolateRight: 'clamp',
3886
+ });
3887
+ \`\`\`
3854
3888
 
3855
- return (
3856
- <span>
3857
- {text.slice(0, charIndex)}
3858
- {showCursor && <span style={{ opacity: 0.8 }}>|</span>}
3859
- </span>
3860
- );
3861
- };
3889
+ ---
3890
+
3891
+ ## Background Breathing
3892
+
3893
+ Background is NEVER static.
3894
+
3895
+ \`\`\`tsx
3896
+ const orb1X = Math.sin(frame / 60) * 200;
3897
+ const orb1Y = Math.cos(frame / 80) * 100;
3862
3898
  \`\`\`
3863
3899
 
3864
- ### The "Card Fan"
3900
+ ---
3865
3901
 
3866
- Stack cards, then fan out:
3902
+ ## Typewriter Effect
3867
3903
 
3868
3904
  \`\`\`tsx
3869
- const cards = [
3870
- { rotation: -5, x: -20 },
3871
- { rotation: 0, x: 0 },
3872
- { rotation: 5, x: 20 },
3873
- ];
3905
+ const charIndex = Math.floor(frame / 3);
3906
+ const showCursor = Math.floor(frame / 15) % 2 === 0;
3874
3907
 
3875
- {cards.map((card, i) => {
3876
- const fanProgress = spring({ frame: frame - 30, fps, config: { damping: 15, stiffness: 100 } });
3908
+ <span>
3909
+ {text.slice(0, charIndex)}
3910
+ {showCursor && <span>|</span>}
3911
+ </span>
3912
+ \`\`\`
3877
3913
 
3878
- return (
3879
- <div style={{
3880
- position: 'absolute',
3881
- transform: \`rotate(\${fanProgress * card.rotation}deg) translateX(\${fanProgress * card.x}px)\`,
3882
- zIndex: i,
3883
- }}>
3884
- <ProfileCard />
3885
- </div>
3886
- );
3887
- })}
3914
+ ---
3915
+
3916
+ ## Vignette & Noise
3917
+
3918
+ \`\`\`tsx
3919
+ // Noise
3920
+ <AbsoluteFill style={{
3921
+ backgroundImage: 'url(/noise.png)',
3922
+ opacity: 0.03,
3923
+ mixBlendMode: 'overlay',
3924
+ }} />
3925
+
3926
+ // Vignette
3927
+ <AbsoluteFill style={{
3928
+ background: 'radial-gradient(ellipse at center, transparent 40%, rgba(0,0,0,0.8) 100%)',
3929
+ }} />
3888
3930
  \`\`\`
3931
+ `
3932
+ },
3933
+ {
3934
+ filename: "advanced-techniques.md",
3935
+ content: `# Advanced Techniques
3936
+
3937
+ ## Audio-Reactive
3938
+
3939
+ - **Kick:** \`scale(1.005)\` pulse
3940
+ - **Snare:** Trigger scene changes
3941
+ - **Hi-hats:** Cursor flicker, particle shimmer
3889
3942
 
3890
3943
  ---
3891
3944
 
3892
- ## 7. Strict Animation Guidelines (DO NOT DEVIATE)
3945
+ ## Motion Blur (Fake)
3893
3946
 
3894
- **ROLE:** Senior Motion Graphics Engineer for Remotion.
3895
- **REFERENCE STYLE:** "High-End SaaS Product Launch" (e.g., Affable.ai, Linear.app).
3947
+ \`\`\`tsx
3948
+ // Trail: Render 3-4 copies with opacity 0.3, 1-frame delay
3896
3949
 
3897
- ### Physics & Timing
3950
+ // Or drop shadow for fast movement:
3951
+ filter: \`drop-shadow(\${velocityX * 0.5}px \${velocityY * 0.5}px 10px rgba(0,0,0,0.3))\`
3952
+ \`\`\`
3898
3953
 
3899
- - **No Linear Motion:** All spatial movement (X, Y, Scale) must use \`spring\`.
3900
- - *Standard Spring:* \`{ mass: 1, damping: 15, stiffness: 120 }\` (Snappy but smooth)
3901
- - *Heavy Spring (Mockups):* \`{ mass: 2, damping: 20, stiffness: 100 }\` (Feels weighty)
3902
- - **Continuous Flow:** Use \`interpolate(frame)\` to ensure \`scale\` or \`rotation\` is changing slightly on EVERY frame. No static resting states.
3954
+ ---
3903
3955
 
3904
- ### UI Component Behavior
3956
+ ## 3D Perspective
3905
3957
 
3906
- - **Entrance:** When a UI card enters, it must use **2.5D Rotation**.
3907
- - *Code:* \`transform: perspective(1000px) rotateX(\${rotateX}deg) translateY(\${y}px)\`
3908
- - *Values:* Start at \`rotateX(20deg)\` and \`y(100px)\`. Spring to \`0\`.
3909
- - **Internal Staggering:** If the UI card contains a list or grid, use \`<Sequence>\` or \`delay\` to reveal items one by one (3 frame gap).
3910
- - **Cursor Interaction:** If a cursor clicks a button:
3911
- 1. Cursor scales \`1 -> 0.8\`
3912
- 2. Button scales \`1 -> 0.95\`
3913
- 3. Release both to \`1\` with a spring
3958
+ \`\`\`tsx
3959
+ <div style={{ perspective: '1000px' }}>
3960
+ <div style={{
3961
+ transform: 'rotateX(5deg) rotateY(10deg)',
3962
+ transformStyle: 'preserve-3d',
3963
+ }}>
3964
+ {/* Your UI */}
3965
+ </div>
3966
+ </div>
3967
+ \`\`\`
3914
3968
 
3915
- ### Typography Patterns
3969
+ ---
3916
3970
 
3917
- - **Pattern A (Headline):** "Masked Slide Up". Text translates Y from \`100%\` inside a clipped container.
3918
- - **Pattern B (Keywords):** "Scale & Glow". Keywords scale to \`1.1\` and emit a \`text-shadow\`.
3919
- - **Colors:** Text is never pure white (\`#FFF\`). Use \`#F0F0F0\` with a subtle gradient or shadow to add depth.
3971
+ ## Kinetic Typography
3920
3972
 
3921
- ### Background "Aliveness"
3973
+ ### Masked Reveal
3974
+ \`\`\`tsx
3975
+ <div style={{ overflow: 'hidden', height: 80 }}>
3976
+ <h1 style={{
3977
+ transform: \`translateY(\${interpolate(progress, [0, 1], [100, 0])}%)\`,
3978
+ }}>
3979
+ INTRODUCING
3980
+ </h1>
3981
+ </div>
3982
+ \`\`\`
3922
3983
 
3923
- - Create a \`<MovingBackground>\` component.
3924
- - It must feature 2-3 \`AbsoluteFill\` gradient orbs that move in a continuous loop using \`Math.sin(frame / slowFactor)\`.
3925
- - This ensures the video has "depth" behind the text.
3984
+ ### Keyword Animation
3985
+ Animate keywords, not whole sentences.
3986
+ `
3987
+ },
3988
+ {
3989
+ filename: "remotion-config.md",
3990
+ content: `# Remotion Configuration
3991
+
3992
+ ## FPS & Resolution
3993
+
3994
+ - **60 FPS mandatory** \u2014 30fps looks choppy
3995
+ - **1920\xD71080** Full HD
3996
+ - **Center:** \`{x: 960, y: 540}\`
3926
3997
 
3927
3998
  ---
3928
3999
 
3929
- ## 8. Seamless Scene Transitions (No Hard Cuts)
4000
+ ## Timing
4001
+
4002
+ - 1 second = 60 frames
4003
+ - Fast interaction = 15-20 frames
4004
+ - No scene > 8 seconds without action
3930
4005
 
3931
- In high-end motion design, transitions are rarely separate "effects" (like a cross-dissolve). The *outgoing* scene physically pushes, slides, or zooms into the *incoming* scene.
4006
+ ---
3932
4007
 
3933
- ### The "Wipe" Rule (No Hard Cuts)
4008
+ ## Entry-Action-Exit Structure
3934
4009
 
3935
- - **Forbidden:** Never let Scene A end at frame X and Scene B start at frame X+1 with a simple cut.
3936
- - **Required:** Scene B must exist *above* Scene A (z-index) for at least 15-20 frames before the cut happens.
3937
- - **Mechanism:** Scene B enters using a "Wipe" motion (sliding in from the bottom/right) or a "Scale In" (expanding from a dot).
4010
+ | Phase | Duration |
4011
+ |-------|----------|
4012
+ | Entry | 0.0s - 0.5s |
4013
+ | Action | 0.5s - (duration - 1s) |
4014
+ | Exit | last 1s |
3938
4015
 
3939
- ### The "Zoom-Through" Technique (Best for SaaS)
4016
+ ---
3940
4017
 
3941
- As Scene A ends, the camera must **accelerate** its zoom:
4018
+ ## Font Loading
3942
4019
 
3943
4020
  \`\`\`tsx
3944
- // Scene A Action: At duration - 20 frames, zoom way in
3945
- const scaleA = interpolate(frame, [duration - 20, duration], [1.0, 5.0]);
4021
+ const [handle] = useState(() => delayRender());
3946
4022
 
3947
- // Scene B Action: Starts at scale 0.5 and springs to 1.0
3948
- const scaleB = spring({ frame, fps, config: { damping: 14, stiffness: 120 } });
3949
- const scaleBValue = interpolate(scaleB, [0, 1], [0.5, 1]);
4023
+ useEffect(() => {
4024
+ document.fonts.ready.then(() => {
4025
+ continueRender(handle);
4026
+ });
4027
+ }, [handle]);
3950
4028
  \`\`\`
3951
4029
 
3952
- Result: It looks like the camera flew *through* Scene A to find Scene B behind it.
3953
-
3954
- ### The "Color Swipe" (Safety Net)
4030
+ ---
3955
4031
 
3956
- If a complex match-cut is too hard, use a "Curtain":
4032
+ ## Zod Schema
3957
4033
 
3958
4034
  \`\`\`tsx
3959
- // A solid colored AbsoluteFill slides in from left, covers screen for 2 frames,
3960
- // then slides out to the right, revealing Scene B
3961
- const curtainX = interpolate(frame, [0, 10, 12, 22], [-100, 0, 0, 100]);
4035
+ export const SceneSchema = z.object({
4036
+ titleText: z.string(),
4037
+ buttonColor: z.string(),
4038
+ cursorPath: z.array(z.object({ x: z.number(), y: z.number() })),
4039
+ });
3962
4040
  \`\`\`
3963
4041
 
3964
- ### TransitionWrapper Component
4042
+ ---
3965
4043
 
3966
- Use the \`TransitionWrapper\` from shared components:
4044
+ ## SaaS Video Kit Components
3967
4045
 
3968
- \`\`\`tsx
3969
- import { TransitionWrapper } from './shared';
4046
+ | Component | Purpose |
4047
+ |-----------|---------|
4048
+ | \`MockWindow\` | macOS window with traffic lights |
4049
+ | \`SmartCursor\` | Bezier curves + click physics |
4050
+ | \`NotificationToast\` | Slide in, wait, slide out |
4051
+ | \`TypingText\` | Typewriter with cursor |
4052
+ | \`Placeholder\` | For logos/icons |
3970
4053
 
3971
- // Types: 'slide' | 'zoom' | 'fade' | 'none'
3972
- // enterFrom: 'left' | 'right' | 'bottom'
4054
+ ---
3973
4055
 
3974
- <TransitionWrapper type="slide" enterFrom="bottom">
3975
- <YourScene />
3976
- </TransitionWrapper>
3977
- \`\`\`
4056
+ ## Code Rules
3978
4057
 
3979
- ### Critical: Overlap Your Sequences
4058
+ 1. No \`transition: all 0.3s\` \u2014 use \`interpolate()\` or \`spring()\`
4059
+ 2. Use \`AbsoluteFill\` for layout
4060
+ 3. No magic numbers \u2014 extract to constants
4061
+ `
4062
+ },
4063
+ {
4064
+ filename: "elite-production.md",
4065
+ content: `# Elite Production
4066
+
4067
+ For Stripe/Apple/Linear quality.
4068
+
4069
+ ---
3980
4070
 
3981
- **This is the most important part.** To make transitions work, you must overlap sequences slightly. If Scene 1 is 90 frames, start Scene 2 at frame 75 (15-frame overlap).
4071
+ ## Global Lighting Engine
3982
4072
 
3983
4073
  \`\`\`tsx
3984
- export const MyVideo = () => {
3985
- return (
3986
- <AbsoluteFill>
3987
- {/* SCENE 1: Ends at frame 100 */}
3988
- <Sequence from={0} durationInFrames={100}>
3989
- <SceneOne />
3990
- </Sequence>
3991
-
3992
- {/* SCENE 2: Starts at frame 85 (15 frames early!) */}
3993
- {/* TransitionWrapper slides it ON TOP of Scene 1 */}
3994
- <Sequence from={85} durationInFrames={150}>
3995
- <TransitionWrapper type="slide" enterFrom="bottom">
3996
- <SceneTwo />
3997
- </TransitionWrapper>
3998
- </Sequence>
3999
-
4000
- {/* SCENE 3: Starts early again */}
4001
- <Sequence from={220} durationInFrames={150}>
4002
- <TransitionWrapper type="zoom">
4003
- <SceneThree />
4004
- </TransitionWrapper>
4005
- </Sequence>
4006
- </AbsoluteFill>
4007
- );
4008
- };
4074
+ const lightSource = { x: 0.2, y: -0.5 };
4075
+ const gradientAngle = Math.atan2(lightSource.y, lightSource.x) * (180 / Math.PI);
4076
+
4077
+ <button style={{
4078
+ background: \`linear-gradient(\${gradientAngle}deg, rgba(255,255,255,0.1) 0%, transparent 50%)\`,
4079
+ borderTop: '1px solid rgba(255,255,255,0.15)',
4080
+ boxShadow: \`\${-lightSource.x * 20}px \${-lightSource.y * 20}px 40px rgba(0,0,0,0.3)\`,
4081
+ }} />
4009
4082
  \`\`\`
4010
4083
 
4011
- **Why this fixes the "Light Switch" effect:** Because Scene 2 physically slides *over* Scene 1 while Scene 1 is still visible underneath, your brain registers it as continuous movement rather than a "cut."
4084
+ ---
4085
+
4086
+ ## Noise & Dithering
4087
+
4088
+ Every background needs noise overlay (opacity 0.02-0.05). Prevents YouTube banding.
4012
4089
 
4013
4090
  ---
4014
4091
 
4015
- ## Quick Tips from Motion Devs
4092
+ ## React Three Fiber
4093
+
4094
+ For particles, 3D globes \u2014 use WebGL via \`@remotion/three\`, not CSS 3D.
4095
+
4096
+ \`\`\`tsx
4097
+ import { ThreeCanvas } from '@remotion/three';
4098
+
4099
+ <AbsoluteFill>
4100
+ <HtmlUI />
4101
+ <ThreeCanvas>
4102
+ <Particles />
4103
+ </ThreeCanvas>
4104
+ </AbsoluteFill>
4105
+ \`\`\`
4016
4106
 
4017
- - **Don't use white backgrounds.** The 2.5D lighting and shadows pop much better on dark grey or black backgrounds.
4018
- - **Overlap delays.** If Title starts at frame 10 and Card starts at frame 30, the title hasn't finished moving when the card appears. This overlap is crucial for fluidity.
4019
- - **High FPS.** Ensure your \`remotion.config.ts\` is set to 60fps for springs to look smooth.
4107
+ See \`threejs-*\` skills for implementation.
4020
4108
 
4021
4109
  ---
4022
4110
 
4023
- ## Self-Check: Is It Kinetic?
4111
+ ## Virtual Camera Rig
4024
4112
 
4025
- Before rendering, verify:
4113
+ Move camera, not elements:
4026
4114
 
4027
- - [ ] Camera rig wraps entire scene with drift/zoom
4028
- - [ ] Every UI element uses 2.5D rotation entrance
4029
- - [ ] Cursor moves in curves with overshoot
4030
- - [ ] Lists/grids stagger (never appear all at once)
4031
- - [ ] Text uses masked reveal or keyword animation
4032
- - [ ] Background has moving orbs + vignette + noise
4033
- - [ ] Something is moving on EVERY frame
4034
- - [ ] No static resting states longer than 30 frames
4035
- - [ ] Scene transitions overlap (no hard cuts between scenes)
4036
- - [ ] TransitionWrapper used for slide/zoom entrances
4115
+ \`\`\`tsx
4116
+ const CameraProvider = ({ children }) => {
4117
+ const frame = useCurrentFrame();
4118
+ const panX = interpolate(frame, [0, 300], [0, -100]);
4119
+ const zoom = interpolate(frame, [0, 300], [1, 1.05]);
4120
+
4121
+ return (
4122
+ <div style={{
4123
+ transform: \`translateX(\${panX}px) scale(\${zoom})\`,
4124
+ transformOrigin: 'center',
4125
+ }}>
4126
+ {children}
4127
+ </div>
4128
+ );
4129
+ };
4130
+ \`\`\`
4131
+
4132
+ ---
4037
4133
 
4038
- If your video looks like PowerPoint slides with voiceover, **START OVER**.
4134
+ ## Motion Rules
4135
+
4136
+ - **Overshoot:** Modal scales to 1.02, settles to 1.0
4137
+ - **Overlap:** Scene B starts 15 frames before Scene A ends
4039
4138
  `
4040
4139
  },
4041
4140
  {
4042
- filename: "failures.md",
4043
- content: `# Common Failures - READ THIS FIRST
4141
+ filename: "known-issues.md",
4142
+ content: `# Known Issues & Fixes
4044
4143
 
4045
- If your video looks like any of these, START OVER.
4144
+ ## 1. Music Ends Before Video Finishes
4046
4145
 
4047
- ## FAILURE 1: Slideshow
4146
+ **Problem:** Music duration is shorter than video duration, causing awkward silence at the end.
4048
4147
 
4049
- \`\`\`
4050
- What you made:
4051
- - Background image
4052
- - Text appears on top
4053
- - Text disappears
4054
- - New background image
4055
- - More text
4056
-
4057
- This is PowerPoint, not a video. REJECTED.
4058
- \`\`\`
4148
+ **Solution:** Loop music in Remotion using the \`loop\` prop:
4059
4149
 
4060
- ## FAILURE 2: Lorem Ipsum / Placeholder Content
4150
+ \`\`\`tsx
4151
+ import { Audio } from 'remotion';
4061
4152
 
4153
+ <Audio src={musicSrc} volume={0.3} loop />
4062
4154
  \`\`\`
4063
- What you made:
4064
- - "Lorem ipsum dolor sit amet..."
4065
- - "Sample text here"
4066
- - Generic placeholder content
4067
4155
 
4068
- Use REAL content from the project. REJECTED.
4069
- \`\`\`
4156
+ **How it works:**
4157
+ - Music automatically loops to fill video duration
4158
+ - Set volume to 0.3 (30% - less loud than voice)
4159
+ - Add fade out at the end for smooth ending
4070
4160
 
4071
- ## FAILURE 3: Static UI Screenshot
4161
+ ---
4072
4162
 
4073
- \`\`\`
4074
- What you made:
4075
- - Screenshot of the app
4076
- - Text overlay saying "Our Dashboard"
4077
- - No motion, no interaction
4163
+ ## 2. Music Transitions Sound Abrupt
4078
4164
 
4079
- Recreate the UI in code and ANIMATE it. REJECTED.
4080
- \`\`\`
4165
+ **Problem:** Music cuts harshly when scenes change or video ends.
4081
4166
 
4082
- ## FAILURE 4: Elements Just Appearing
4167
+ **Fix in Remotion:**
4168
+ \`\`\`tsx
4169
+ import { interpolate, Audio } from 'remotion';
4083
4170
 
4084
- \`\`\`
4085
- What you made:
4086
- - Frame 0: nothing
4087
- - Frame 1: element is fully visible
4088
- - No transition, no animation
4171
+ // Fade music in/out at scene boundaries
4172
+ const musicVolume = interpolate(
4173
+ frame,
4174
+ [0, 30, totalFrames - 60, totalFrames],
4175
+ [0, 0.3, 0.3, 0],
4176
+ { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }
4177
+ );
4089
4178
 
4090
- Every element must animate in with spring/easing. REJECTED.
4179
+ <Audio src={music} volume={musicVolume} />
4091
4180
  \`\`\`
4092
4181
 
4093
- ## FAILURE 5: Hard Cuts Between Scenes (The "Light Switch" Effect)
4182
+ ---
4094
4183
 
4095
- \`\`\`
4096
- What you made:
4097
- - Scene 1 ends at frame 100
4098
- - Scene 2 starts at frame 101
4099
- - No overlap, no slide, no zoom-through
4100
- - Feels like a light switching on/off
4101
-
4102
- FIX: Overlap sequences by 15-20 frames. Scene 2 must slide/zoom
4103
- ON TOP of Scene 1 while Scene 1 is still visible. Use TransitionWrapper.
4104
- REJECTED.
4105
- \`\`\`
4184
+ ## 3. Scene Transitions Too Harsh
4106
4185
 
4107
- ## FAILURE 6: Static Camera
4186
+ **Problem:** Scenes change abruptly without smooth transitions.
4108
4187
 
4188
+ **Fix:** Use \`@remotion/transitions\` with overlapping:
4189
+ \`\`\`tsx
4190
+ import { TransitionSeries, springTiming } from '@remotion/transitions';
4191
+ import { slide } from '@remotion/transitions/slide';
4192
+ import { fade } from '@remotion/transitions/fade';
4193
+
4194
+ <TransitionSeries>
4195
+ <TransitionSeries.Sequence durationInFrames={sceneA.frames}>
4196
+ <SceneA />
4197
+ </TransitionSeries.Sequence>
4198
+ <TransitionSeries.Transition
4199
+ presentation={slide({ direction: 'from-right' })}
4200
+ timing={springTiming({ config: { damping: 20 } })}
4201
+ />
4202
+ <TransitionSeries.Sequence durationInFrames={sceneB.frames}>
4203
+ <SceneB />
4204
+ </TransitionSeries.Sequence>
4205
+ </TransitionSeries>
4109
4206
  \`\`\`
4110
- What you made:
4111
- - Camera never moves
4112
- - No drift, no zoom, no rotation
4113
- - Feels like watching a screenshot
4114
4207
 
4115
- Camera must ALWAYS be subtly moving. REJECTED.
4116
- \`\`\`
4208
+ ---
4117
4209
 
4118
- ## FAILURE 7: Linear Cursor Movement
4210
+ ## 4. Voiceover Lacks Energy
4119
4211
 
4120
- \`\`\`
4121
- What you made:
4122
- - Cursor moves in straight lines
4123
- - No overshoot on stops
4124
- - Click has no feedback
4212
+ **Problem:** Voiceover sounds flat/monotone.
4125
4213
 
4126
- Cursor must move in Bezier curves with overshoot. REJECTED.
4214
+ **Fix:** Pass \`voiceSettings\` in scenes JSON:
4215
+ \`\`\`json
4216
+ {
4217
+ "scenes": [...],
4218
+ "voice": "Kore",
4219
+ "voiceSettings": {
4220
+ "style": 0.6,
4221
+ "stability": 0.4,
4222
+ "speed": 0.95
4223
+ }
4224
+ }
4127
4225
  \`\`\`
4128
- `
4129
- },
4130
- {
4131
- filename: "parameterization.md",
4132
- content: `# Parameterization (Critical)
4133
-
4134
- Never hardcode frame numbers. Use variables for all keyframes.
4135
4226
 
4136
- ## Why
4227
+ - \`style\`: 0.5-0.7 for more expressive delivery
4228
+ - \`stability\`: 0.3-0.5 for more variation
4229
+ - \`speed\`: 0.9-1.0 slightly slower = more impactful
4137
4230
 
4138
- When you change timing early in video, everything else breaks if hardcoded.
4231
+ ---
4139
4232
 
4140
- ## Pattern
4233
+ ## 5. Video Duration Mismatch
4141
4234
 
4142
- \`\`\`ts
4143
- // Define keyframes as variables
4144
- const SCENE_START = 0;
4145
- const TITLE_IN = SCENE_START + 15;
4146
- const TITLE_HOLD = TITLE_IN + 60;
4147
- const TITLE_OUT = TITLE_HOLD + 15;
4148
- const SUBTITLE_IN = TITLE_IN + 20; // Relative to title
4149
- const SCENE_END = TITLE_OUT + 30;
4235
+ **Problem:** Brief says 30-45s but video is 20s (because scene duration = voiceover duration).
4150
4236
 
4151
- // Use in animations
4152
- opacity: interpolate(frame, [TITLE_IN, TITLE_IN + 15], [0, 1])
4237
+ **Fixes:**
4238
+ 1. **Slow voice:** Use \`speed: 0.85\` in voiceSettings
4239
+ 2. **Add padding in Remotion:** Hold last frame, add breathing room
4240
+ \`\`\`tsx
4241
+ // Add 30 frames (0.5s) padding after voiceover ends
4242
+ const paddedDuration = voiceoverFrames + 30;
4153
4243
  \`\`\`
4244
+ 3. **Brief should note:** "Duration based on voiceover length"
4154
4245
 
4155
- ## Scene Duration
4246
+ ---
4156
4247
 
4157
- \`\`\`ts
4158
- // Calculate from keyframes, don't hardcode
4159
- const sceneDuration = SCENE_END - SCENE_START;
4160
- \`\`\`
4248
+ ## 6. Not Using Project UI Components
4161
4249
 
4162
- ## Audio Sync
4250
+ **Problem:** Generic UI instead of pixel-perfect project components.
4163
4251
 
4164
- \`\`\`ts
4165
- // If audio timestamp changes, only update one variable
4166
- const VOICEOVER_START = 45;
4167
- const VISUAL_CUE = VOICEOVER_START + 10; // Auto-adjusts
4252
+ **Fix:** In Phase 1 Discovery:
4253
+ 1. Find project's actual components (buttons, cards, modals, inputs)
4254
+ 2. Copy their styles/structure into Remotion components
4255
+ 3. Match colors, fonts, shadows, border-radius exactly
4256
+
4257
+ \`\`\`tsx
4258
+ // DON'T: Generic button
4259
+ <button style={{ background: 'blue' }}>Click</button>
4260
+
4261
+ // DO: Match project's actual button
4262
+ <button style={{
4263
+ background: 'linear-gradient(135deg, #6366f1, #8b5cf6)',
4264
+ borderRadius: 8,
4265
+ padding: '12px 24px',
4266
+ boxShadow: '0 4px 14px rgba(99, 102, 241, 0.4)',
4267
+ border: '1px solid rgba(255,255,255,0.1)',
4268
+ }}>Click</button>
4168
4269
  \`\`\`
4169
4270
 
4170
- ## Multi-Scene
4271
+ ---
4171
4272
 
4172
- \`\`\`ts
4173
- const SCENE_1_START = 0;
4174
- const SCENE_1_END = SCENE_1_START + 90;
4175
- const SCENE_2_START = SCENE_1_END - 15; // 15 frame overlap for transition
4176
- const SCENE_2_END = SCENE_2_START + 120;
4177
- \`\`\`
4273
+ ## 7. Missing Physics & Lighting
4274
+
4275
+ **Problem:** Video feels flat, no depth or motion.
4276
+
4277
+ **Checklist:**
4278
+ - [ ] Global light source defined (affects all shadows/gradients)
4279
+ - [ ] Camera rig with subtle drift/zoom
4280
+ - [ ] Spring physics on ALL entrances (no linear)
4281
+ - [ ] Staggered animations (never all at once)
4282
+ - [ ] Background orbs/particles moving
4283
+ - [ ] Noise overlay (opacity 0.02-0.05)
4284
+ - [ ] Vignette for depth
4178
4285
  `
4179
- },
4180
- {
4181
- filename: "layers.md",
4182
- content: `# Layers & Composition
4286
+ }
4287
+ ];
4183
4288
 
4184
- Explicit z-index prevents visual bugs.
4289
+ // src/commands/skill/generate-video-skill.ts
4290
+ function generateVideoSkillContent(context) {
4291
+ const { name, cmd: cmd2, displayName } = context;
4292
+ return `---
4293
+ name: ${name}-video
4294
+ description: Use when user asks to create videos (product demos, explainers, social content, promos). Handles video asset generation, Remotion implementation, and thumbnail embedding.
4295
+ ---
4185
4296
 
4186
- ## Z-Index Scale
4297
+ # ${displayName} Video Creation CLI
4187
4298
 
4188
- | Layer | Z-Index | Examples |
4189
- |-------|---------|----------|
4190
- | Background orbs | 0 | Moving gradients, noise |
4191
- | Vignette | 1 | Dark corners overlay |
4192
- | UI Base | 10 | Main UI container |
4193
- | UI Elements | 20 | Buttons, cards, inputs |
4194
- | Overlays | 30 | Modals, tooltips, highlights |
4195
- | Text/Captions | 40 | Titles, labels |
4196
- | Cursor | 50 | Mouse pointer |
4197
- | Debug | 100 | Frame counter (remove in final) |
4299
+ 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.
4198
4300
 
4199
- ## Composition Structure
4301
+ **Stack:** Remotion (React video framework) + React Three Fiber (R3F) + Three.js for 3D/WebGL, particles, shaders, lighting.
4200
4302
 
4201
- \`\`\`tsx
4202
- <AbsoluteFill>
4203
- {/* Background layer */}
4204
- <MovingBackground />
4303
+ 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.
4205
4304
 
4206
- {/* Vignette */}
4207
- <Vignette />
4305
+ **Core Philosophy:** "Nothing sits still. Everything is physics-based. Every pixel breathes."
4208
4306
 
4209
- {/* Camera rig wraps all content */}
4210
- <CameraRig>
4211
- <Sequence from={0}>
4212
- <Scene1 />
4213
- </Sequence>
4214
- <Sequence from={SCENE_2_START}>
4215
- <Scene2 />
4216
- </Sequence>
4217
- </CameraRig>
4307
+ ---
4218
4308
 
4219
- {/* Audio */}
4220
- <Audio src={music} volume={0.3} />
4221
- <Audio src={voiceover} />
4222
- </AbsoluteFill>
4223
- \`\`\`
4309
+ ## CRITICAL: Professional Composition Rules
4310
+
4311
+ **These rules are MANDATORY for all marketing/product videos:**
4312
+
4313
+ ### \u274C NEVER DO:
4314
+ 1. **Walls of text** - No dense paragraphs or lists longer than 3 lines
4315
+ 2. **Flying/floating cards** - No random floating animations across the screen
4316
+ 3. **Stretched layouts** - No elements awkwardly stretched to fill space
4317
+ 4. **Truncated text** - Never show "Text that gets cut off..."
4318
+ 5. **Information overload** - Max 1-2 key points visible at once
4319
+ 6. **Amateur motion** - No PowerPoint-style "fly in from left/right"
4224
4320
 
4225
- ## Overlapping Sequences
4321
+ ### \u2705 ALWAYS DO:
4322
+ 1. **Hierarchy first** - One clear focal point per scene (headline OR stat OR visual, not all)
4323
+ 2. **Breathing room** - Generous whitespace (min 100px padding around elements)
4324
+ 3. **Purposeful motion** - Cards appear with subtle spring (0-20px translateY), not fly across screen
4325
+ 4. **Readable text** - Max 2-3 lines per card, 24px+ font size
4326
+ 5. **Grid alignment** - Use invisible grid (3-column or 4-column layout)
4327
+ 6. **Professional entrance** - Elements fade + slight translate (15px max), hold for 2-3s, then exit
4226
4328
 
4227
- For zoom-through transitions, scenes overlap:
4329
+ ### Composition Examples:
4228
4330
 
4331
+ **\u274C BAD - Wall of Text:**
4229
4332
  \`\`\`tsx
4230
- <Sequence from={0} durationInFrames={100}>
4231
- <Scene1 />
4232
- </Sequence>
4233
- <Sequence from={80} durationInFrames={100}> {/* 20 frame overlap */}
4234
- <Scene2 />
4235
- </Sequence>
4333
+ // DON'T: 10 bullet points crammed in a card
4334
+ <Card>
4335
+ <ul>
4336
+ {[...10items].map(item => <li>{item.longText}...</li>)}
4337
+ </ul>
4338
+ </Card>
4236
4339
  \`\`\`
4237
- `
4238
- },
4239
- {
4240
- filename: "project-based.md",
4241
- content: `# Component Extraction (Critical for Product Videos)
4242
4340
 
4243
- When creating a video for a project, **copy the actual components** - don't rebuild from scratch.
4341
+ **\u2705 GOOD - Single Focus:**
4342
+ \`\`\`tsx
4343
+ // DO: One headline, one supporting stat
4344
+ <AbsoluteFill style={{ alignItems: 'center', justifyContent: 'center' }}>
4345
+ <h1 style={{ fontSize: 72, marginBottom: 40 }}>12 hours wasted</h1>
4346
+ <p style={{ fontSize: 28, opacity: 0.7 }}>per week on manual tasks</p>
4347
+ </AbsoluteFill>
4348
+ \`\`\`
4244
4349
 
4245
- ## The Goal
4350
+ **\u274C BAD - Flying Cards:**
4351
+ \`\`\`tsx
4352
+ // DON'T: Cards flying from random positions
4353
+ <Card style={{
4354
+ transform: \`translateX(\${interpolate(progress, [0,1], [-500, 0])}px)\` // Flies from left
4355
+ }} />
4356
+ \`\`\`
4246
4357
 
4247
- Your video should show the REAL product UI, pixel-perfect. Viewers should not be able to tell the difference between a screenshot and your video (except yours is animated with kinetic motion).
4358
+ **\u2705 GOOD - Subtle Entrance:**
4359
+ \`\`\`tsx
4360
+ // DO: Gentle spring entrance with minimal movement
4361
+ const progress = spring({ frame: frame - startFrame, fps, config: { damping: 20, stiffness: 100 }});
4362
+ <Card style={{
4363
+ opacity: progress,
4364
+ transform: \`translateY(\${interpolate(progress, [0,1], [15, 0])}px)\` // Subtle 15px drop
4365
+ }} />
4366
+ \`\`\`
4248
4367
 
4249
- ## Process: Eject \u2192 Simplify \u2192 Animate
4368
+ ### Layout Grid System:
4250
4369
 
4251
- ### Step 1: Find Components to Feature
4370
+ **Use 12-column grid (like Bootstrap):**
4371
+ \`\`\`tsx
4372
+ const GRID = {
4373
+ columns: 12,
4374
+ gutter: 40,
4375
+ padding: 120 // Edge padding
4376
+ };
4252
4377
 
4253
- \`\`\`bash
4254
- # Explore the project structure
4255
- ls src/components/
4256
- ls src/app/
4257
- # Find the key UI: dashboards, forms, cards, modals, etc.
4378
+ // Center 6 columns for main content
4379
+ const contentWidth = (1920 - (GRID.padding * 2) - (GRID.gutter * 5)) / 2;
4258
4380
  \`\`\`
4259
4381
 
4260
- ### Step 2: Copy Component Code
4382
+ **Positioning anchors:**
4383
+ - **Top-left:** Brand logo, context (10% from edges)
4384
+ - **Center:** Primary headline/stat/demo (50% transform)
4385
+ - **Bottom:** CTA or tagline (10% from bottom)
4386
+ - **Never:** Random floating between these zones
4261
4387
 
4262
- Copy the actual component file into your Remotion project.
4388
+ ---
4263
4389
 
4264
- ### Step 3: Eject - Strip Logic, Keep Visuals
4390
+ ## Prerequisites
4265
4391
 
4266
- \`\`\`tsx
4267
- // BEFORE: Original component from project
4268
- export function PricingCard({ plan, onSelect }) {
4269
- const [loading, setLoading] = useState(false);
4270
- const handleClick = async () => { /* API calls */ };
4271
- return <div>...</div>;
4272
- }
4392
+ Before using this skill, ensure you have:
4273
4393
 
4274
- // AFTER: Ejected for Remotion
4275
- export function PricingCard({ plan, progress, isHovered }) {
4276
- // No useState, no API calls, no handlers
4277
- // Just visual + animation props from Remotion
4394
+ 1. **Load related skills:**
4395
+ \`\`\`
4396
+ remotion-best-practices
4397
+ threejs-fundamentals
4398
+ \`\`\`
4278
4399
 
4279
- const entranceProgress = spring({ frame: progress, fps, config: { mass: 2, damping: 20, stiffness: 100 } });
4280
- const rotateX = interpolate(entranceProgress, [0, 1], [20, 0]);
4400
+ 2. **Authenticate:**
4401
+ \`\`\`bash
4402
+ ${cmd2} login
4403
+ \`\`\`
4281
4404
 
4282
- return (
4283
- <div style={{
4284
- transform: \`perspective(1000px) rotateX(\${rotateX}deg)\`,
4285
- // ... exact same visual styles as original
4286
- }}>
4287
- ...
4288
- </div>
4289
- );
4290
- }
4291
- \`\`\`
4405
+ 3. **Remotion installed** (if creating videos):
4406
+ \`\`\`bash
4407
+ pnpm install remotion @remotion/cli
4408
+ \`\`\`
4292
4409
 
4293
- ### Step 4: Add Kinetic Animation
4410
+ ---
4294
4411
 
4295
- Apply the rules from kinetic-saas.md:
4296
- - 2.5D rotation entrance
4297
- - Spring physics
4298
- - Staggered children
4299
- - Cursor interaction states
4412
+ ## Video Creation Workflow
4300
4413
 
4301
- ### Step 5: Fill With Real Demo Data
4414
+ ### Phase 0: Load Skills (MANDATORY)
4302
4415
 
4303
- \`\`\`tsx
4304
- // Use realistic data that matches the product
4305
- const DEMO_PLANS = [
4306
- { name: 'Starter', price: 9, features: ['5 projects', '10GB storage'] },
4307
- { name: 'Pro', price: 29, features: ['Unlimited projects', '100GB storage', 'Priority support'] },
4308
- ];
4309
- // NOT: "Plan A", "$XX/mo", "Feature 1"
4416
+ Before ANY video work, invoke these skills:
4417
+ \`\`\`
4418
+ remotion-best-practices
4419
+ threejs-fundamentals
4310
4420
  \`\`\`
4311
4421
 
4312
- ## What to Copy
4422
+ ### Phase 1: Discovery
4313
4423
 
4314
- | Find in Project | Copy to Video |
4315
- |-----------------|---------------|
4316
- | \`tailwind.config.js\` | Colors, fonts, spacing |
4317
- | \`globals.css\` | CSS variables, font imports |
4318
- | \`/public/logo.svg\` | Actual logo file |
4319
- | \`/src/components/*.tsx\` | Component structure & styles |
4424
+ Explore current directory silently:
4425
+ - Understand what the product does (README, docs, code)
4426
+ - Find branding: logo, colors, fonts
4427
+ - Find UI components to copy into Remotion (buttons, cards, modals, etc.) \u2014 rebuild pixel-perfect, no screenshots
4320
4428
 
4321
- ## Common Mistakes
4429
+ ### Phase 2: Video Brief
4322
4430
 
4323
- \u274C Using placeholder colors like \`#333\`
4324
- \u2705 Using exact project colors from tailwind config
4431
+ Present a brief outline (scenes \u22648s each, duration, assets found) and get user approval before production.
4325
4432
 
4326
- \u274C Generic content: "Feature 1", "Lorem ipsum"
4327
- \u2705 Real content: "Unlimited projects", "Priority support"
4433
+ ### Phase 3: Production
4328
4434
 
4329
- \u274C Building from scratch based on screenshots
4330
- \u2705 Copying actual component code and ejecting it
4435
+ 1. **Generate audio assets** - \`${cmd2} video create\` with scenes JSON
4436
+ - IMPORTANT: Music is generated LAST after all voiceover/audio to ensure exact duration match
4437
+ 2. **Scaffold OUTSIDE project** - \`cd .. && ${cmd2} video init my-video\`
4438
+ 3. **Copy assets + UI components** from project into video project
4439
+ 4. **Implement** - follow rules below
4331
4440
 
4332
- \u274C Static fade-in animations
4333
- \u2705 2.5D rotation with spring physics (kinetic style)
4334
- `
4335
- },
4336
- {
4337
- filename: "social-media.md",
4338
- content: `# Social Media Video Guide (TikTok/Reels/Shorts)
4441
+ ### Phase 4: Render & Thumbnail (REQUIRED)
4442
+
4443
+ \`\`\`bash
4444
+ # 1. Render the video (with voiceover and music already included)
4445
+ pnpm exec remotion render FullVideo
4446
+
4447
+ # 2. ALWAYS embed thumbnail before delivering
4448
+ ${cmd2} video thumbnail out/FullVideo.mp4 --frame 60
4449
+ \`\`\`
4450
+
4451
+ **Note:** Remotion videos include per-scene voiceovers and background music baked in during render.
4339
4452
 
4340
- **Platforms:** TikTok, Instagram Reels, YouTube Shorts (all 9:16 vertical)
4453
+ ---
4454
+
4455
+ ## Asset Generation
4456
+
4457
+ Generate voiceover, music, and visual assets for each scene:
4458
+
4459
+ \`\`\`bash
4460
+ cat <<SCENES | ${cmd2} video create --output ./public
4461
+ {
4462
+ "scenes": [
4463
+ {
4464
+ "name": "Hook",
4465
+ "script": "Watch how we transformed this complex workflow into a single click.",
4466
+ "imageQuery": "modern dashboard interface dark theme",
4467
+ "videoQuery": "abstract tech particles animation"
4468
+ },
4469
+ {
4470
+ "name": "Demo",
4471
+ "script": "Our AI analyzes your data in real-time, surfacing insights that matter.",
4472
+ "imageQuery": "data visualization charts analytics"
4473
+ },
4474
+ {
4475
+ "name": "CTA",
4476
+ "script": "Start your free trial today. No credit card required.",
4477
+ "imageQuery": "call to action button modern"
4478
+ }
4479
+ ],
4480
+ "voice": "Kore",
4481
+ "voiceSettings": {
4482
+ "style": 0.6,
4483
+ "stability": 0.4,
4484
+ "speed": 0.95
4485
+ },
4486
+ "musicPrompt": "upbeat corporate, positive energy, modern synth"
4487
+ }
4488
+ SCENES
4489
+ \`\`\`
4341
4490
 
4342
- **Philosophy:** "Don't make ads. Make TikToks." Your video must look native to the platform - lo-fi beats polished.
4491
+ **Output:**
4492
+ - \`public/audio/Hook.mp3\` - scene voiceovers
4493
+ - \`public/audio/music.mp3\` - background music (30s max)
4494
+ - \`public/video-manifest.json\` - timing and metadata
4495
+ - Stock images/videos (if requested)
4343
4496
 
4344
4497
  ---
4345
4498
 
4346
- ## 1. The "3-Second War"
4499
+ ## Core Video Rules
4347
4500
 
4348
- You have 3 seconds before the thumb scrolls. Win or lose everything.
4501
+ ${VIDEO_RULE_CONTENTS.map((rule) => rule.content).join("\n\n---\n\n")}
4349
4502
 
4350
- ### Hook Requirements (0-3s)
4503
+ ## Useful Commands
4351
4504
 
4352
- - **Text on screen in FRAME 1** - no waiting for audio
4353
- - **Movement** - never start static
4354
- - **Pattern interrupt** - something unexpected
4505
+ \`\`\`bash
4506
+ # Generate video assets
4507
+ ${cmd2} video create < scenes.json
4508
+ cat scenes.json | ${cmd2} video create --output ./public
4355
4509
 
4356
- ### Hook Types
4510
+ # Initialize Remotion project
4511
+ ${cmd2} video init my-video
4357
4512
 
4358
- | Type | Description | Use For |
4359
- |------|-------------|---------|
4360
- | Face Close-Up | Tight on face, zoom out | Personal/story |
4361
- | Green Screen | Speaker + background tweet/article | Commentary |
4362
- | Text Slam | Bold text hits screen with sound | Listicles, tips |
4363
- | Movement | Object thrown, quick zoom | Product demos |
4513
+ # Embed thumbnail
4514
+ ${cmd2} video thumbnail out/video.mp4 --frame 60
4364
4515
 
4365
- ### Audio Hooks
4516
+ # Search for stock assets
4517
+ ${cmd2} images search "mountain landscape" --limit 10
4518
+ ${cmd2} videos search "ocean waves" --limit 5
4366
4519
 
4367
- Bad: "Today I want to talk about..."
4368
- Good: "Stop scrolling if you hate [X]." / "This feels illegal to know."
4520
+ # Generate audio
4521
+ ${cmd2} audio generate "Your script here" --voice Kore
4522
+ ${cmd2} music generate "upbeat corporate" --duration 30
4523
+ \`\`\`
4369
4524
 
4370
4525
  ---
4371
4526
 
4372
- ## 2. Pacing Rules (No Dead Air)
4527
+ ## Best Practices
4373
4528
 
4374
- | Rule | Implementation |
4375
- |------|----------------|
4376
- | Visual change every 2-3s | Cut, zoom, or new element every 60-90 frames |
4377
- | No "Millennial Pause" | Never start with 1-2s of stillness |
4378
- | Remove all breaths | Edit out pauses between sentences |
4379
- | Jump cuts are native | Don't smooth-cut; jump cuts feel authentic |
4529
+ 1. **Keep scenes under 8 seconds** without cuts or major action
4530
+ 2. **Use spring physics** for all animations, never linear
4531
+ 3. **Rebuild UI components** in React/CSS, no screenshots
4532
+ 4. **Test with thumbnail embedding** before delivering
4533
+ 5. **Music volume at 30%** (30-40% less loud than voice)
4534
+ 6. **Read all video rules** in Phase 0 before implementation
4380
4535
 
4381
4536
  ---
4382
4537
 
4383
- ## 3. Native Lo-Fi Aesthetic
4538
+ ## Troubleshooting
4384
4539
 
4385
- | Looks Like TV Ad (Bad) | Looks Native (Good) |
4386
- |------------------------|---------------------|
4387
- | Perfect lighting | Natural/ring light |
4388
- | Broadcast fonts | Platform-native fonts |
4389
- | Smooth transitions | Jump cuts |
4390
- | Color graded | Raw, slightly oversaturated |
4540
+ If you encounter issues:
4541
+ - Check authentication: \`${cmd2} whoami\`
4542
+ - Verify asset generation: check \`video-manifest.json\`
4543
+ - Voiceover flat: increase style (0.5-0.7), decrease stability (0.3-0.5)
4544
+ - Duration mismatch: adjust \`voiceSettings.speed\` or add padding in Remotion
4391
4545
 
4392
- ### Native Font Stack
4546
+ For detailed troubleshooting, see "Known Issues" section above.
4547
+ `;
4548
+ }
4393
4549
 
4394
- \`\`\`tsx
4395
- const NATIVE_FONT = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif";
4550
+ // src/commands/skill/generate-presentation-skill.ts
4551
+ function generatePresentationSkillContent(context) {
4552
+ const { name, cmd: cmd2, displayName } = context;
4553
+ return `---
4554
+ name: ${name}-presentation
4555
+ description: Use when user asks to create presentations (slides, decks, pitch decks). Generates AI-powered presentations with structured content and professional design.
4556
+ ---
4557
+
4558
+ # ${displayName} Presentation CLI
4559
+
4560
+ 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.
4561
+
4562
+ ---
4563
+
4564
+ ## Authentication
4565
+
4566
+ \`\`\`bash
4567
+ # Login via OAuth
4568
+ ${cmd2} login
4569
+
4570
+ # Or set API key
4571
+ export ${name.toUpperCase().replace(/-/g, "_")}_API_KEY="your-key-here"
4396
4572
  \`\`\`
4397
4573
 
4398
4574
  ---
4399
4575
 
4400
- ## 4. Safe Zone Rules (CRITICAL)
4576
+ ## Creating Presentations
4401
4577
 
4402
- Platform UI overlays your content. Plan for it.
4578
+ ### From Text
4403
4579
 
4580
+ \`\`\`bash
4581
+ ${cmd2} create "AI-powered product analytics platform"
4404
4582
  \`\`\`
4405
- 1080x1920 Canvas
4406
- +------------------------------------------+
4407
- | TOP SAFE ZONE (150px) | <- Notch, status bar
4408
- | ----------------------------------------|
4409
- | |
4410
- | CONTENT SAFE ZONE |
4411
- | (60px left, 120px right margins) |
4412
- | |
4413
- | ----------------------------------------|
4414
- | BOTTOM SAFE ZONE (384px / 20%) | <- Captions, username
4415
- | [Icon strip on right: 120px] | <- Like, Comment, Share
4416
- +------------------------------------------+
4583
+
4584
+ ### From File
4585
+
4586
+ \`\`\`bash
4587
+ ${cmd2} create --file product-brief.md
4417
4588
  \`\`\`
4418
4589
 
4419
- ### Safe Zone Values
4590
+ ### From URL
4420
4591
 
4421
- \`\`\`tsx
4422
- const SAFE_ZONE = {
4423
- top: 150, // Notch/status bar
4424
- bottom: 384, // 20% of 1920 - captions/username
4425
- left: 60, // Edge safety
4426
- right: 120, // Like/Comment/Share icons
4427
- };
4592
+ \`\`\`bash
4593
+ ${cmd2} create --url https://company.com/product
4594
+ \`\`\`
4428
4595
 
4429
- // Captions must sit ABOVE bottom safe zone
4430
- const CAPTION_BOTTOM = 350; // px from bottom
4596
+ ### From Piped Content
4597
+
4598
+ \`\`\`bash
4599
+ cat research.txt | ${cmd2} create
4600
+ pbpaste | ${cmd2} create
4431
4601
  \`\`\`
4432
4602
 
4433
- ### SafeZoneGuide Component
4603
+ ### Advanced Options
4434
4604
 
4435
- \`\`\`tsx
4436
- const SafeZoneGuide: React.FC = () => (
4437
- <AbsoluteFill style={{ pointerEvents: "none" }}>
4438
- {/* Top danger zone */}
4439
- <div style={{
4440
- position: "absolute", top: 0, left: 0, right: 0, height: 150,
4441
- backgroundColor: "rgba(255,0,0,0.2)", borderBottom: "2px dashed red",
4442
- }} />
4443
- {/* Bottom danger zone */}
4444
- <div style={{
4445
- position: "absolute", bottom: 0, left: 0, right: 0, height: 384,
4446
- backgroundColor: "rgba(255,0,0,0.2)", borderTop: "2px dashed red",
4447
- }} />
4448
- {/* Right icon strip */}
4449
- <div style={{
4450
- position: "absolute", top: 150, right: 0, bottom: 384, width: 120,
4451
- backgroundColor: "rgba(255,165,0,0.2)", borderLeft: "2px dashed orange",
4452
- }} />
4453
- </AbsoluteFill>
4454
- );
4605
+ \`\`\`bash
4606
+ ${cmd2} create "Topic" \\
4607
+ --slides 10 \\
4608
+ --style professional \\
4609
+ --branding my-brand \\
4610
+ --template minimal \\
4611
+ --output presentation.zip
4455
4612
  \`\`\`
4456
4613
 
4457
4614
  ---
4458
4615
 
4459
- ## 5. Content Formats
4616
+ ## Presentation Options
4460
4617
 
4461
- ### Green Screen (Speaker + Background)
4618
+ - **\`--slides <count>\`** - Number of slides (default: 8-12 based on content)
4619
+ - **\`--style <style>\`** - Presentation style: professional, creative, minimal, corporate
4620
+ - **\`--branding <name>\`** - Use saved branding profile
4621
+ - **\`--template <name>\`** - Design template to use
4622
+ - **\`--output <file>\`** - Export to file (.zip, .pptx, .pdf)
4623
+ - **\`--format <format>\`** - Output format: human, json, quiet
4462
4624
 
4463
- \`\`\`tsx
4464
- <AbsoluteFill>
4465
- <Img src={backgroundImage} style={{ width: "100%", height: "100%", objectFit: "cover" }} />
4466
- <div style={{
4467
- position: "absolute", bottom: 400, left: 40,
4468
- width: 300, height: 400, borderRadius: 20, overflow: "hidden",
4469
- }}>
4470
- <Video src={speakerVideo} />
4471
- </div>
4472
- </AbsoluteFill>
4473
- \`\`\`
4625
+ ---
4474
4626
 
4475
- ### Listicle (Rapid-Fire)
4627
+ ## Managing Presentations
4476
4628
 
4477
- \`\`\`tsx
4478
- const FRAMES_PER_ITEM = 60; // 2 seconds per item
4479
- const currentIndex = Math.floor(frame / FRAMES_PER_ITEM);
4480
- // Slam-in with spring animation for each item
4481
- \`\`\`
4629
+ \`\`\`bash
4630
+ # List all presentations
4631
+ ${cmd2} list
4632
+ ${cmd2} list --format json
4482
4633
 
4483
- ### Visual ASMR (No Talking)
4634
+ # Get presentation details
4635
+ ${cmd2} get <id-or-slug>
4484
4636
 
4485
- - No voiceover, music only
4486
- - Slow, deliberate cursor movements
4487
- - Satisfying click feedback
4488
- - Ken Burns zoom on details
4637
+ # Export presentation
4638
+ ${cmd2} export <id-or-slug> -o presentation.zip
4489
4639
 
4490
- ### POV Skit
4640
+ # Import presentation
4641
+ ${cmd2} import ./presentation.zip
4491
4642
 
4492
- \`\`\`tsx
4493
- <div style={{ position: "absolute", top: 160, left: 40, right: 120, fontSize: 42 }}>
4494
- POV: {setup}
4495
- </div>
4643
+ # Delete presentation
4644
+ ${cmd2} delete <id-or-slug>
4496
4645
  \`\`\`
4497
4646
 
4498
4647
  ---
4499
4648
 
4500
- ## 6. TikTok Captions
4649
+ ## Branding Management
4501
4650
 
4502
- Use \`@remotion/captions\` with \`createTikTokStyleCaptions\`:
4651
+ \`\`\`bash
4652
+ # List saved brands
4653
+ ${cmd2} branding list
4503
4654
 
4504
- \`\`\`tsx
4505
- import { createTikTokStyleCaptions } from "@remotion/captions";
4655
+ # Extract branding from website
4656
+ ${cmd2} branding extract https://company.com
4506
4657
 
4507
- const { pages } = createTikTokStyleCaptions({
4508
- captions: transcriptCaptions,
4509
- combineTokensWithinMilliseconds: 1200, // Higher = more words per page
4510
- });
4658
+ # Use branding in presentation
4659
+ ${cmd2} create "Topic" --branding company-brand
4511
4660
  \`\`\`
4512
4661
 
4513
- ### Word-by-Word Highlighting
4662
+ ---
4514
4663
 
4515
- \`\`\`tsx
4516
- {page.tokens.map((token) => {
4517
- const isActive = currentTimeMs >= token.fromMs && currentTimeMs < token.toMs;
4518
- return (
4519
- <span style={{
4520
- color: isActive ? "#FFD700" : "#fff",
4521
- fontSize: 64,
4522
- fontWeight: 900,
4523
- textTransform: "uppercase",
4524
- }}>
4525
- {token.text}
4526
- </span>
4527
- );
4528
- })}
4664
+ ## Stock Asset Search
4665
+
4666
+ \`\`\`bash
4667
+ # Search for images
4668
+ ${cmd2} images search "mountain landscape" --limit 10
4669
+ ${cmd2} images search "business team" --format json
4670
+
4671
+ # Search for videos
4672
+ ${cmd2} videos search "ocean waves" --limit 5
4673
+ ${cmd2} videos search "city timelapse" --orientation landscape
4529
4674
  \`\`\`
4530
4675
 
4531
4676
  ---
4532
4677
 
4533
- ## 7. Script Structure (60 Seconds)
4678
+ ## Best Practices
4534
4679
 
4535
- | Timestamp | Section | Purpose |
4536
- |-----------|---------|---------|
4537
- | 0:00-0:03 | **HOOK** | Pattern interrupt, bold claim |
4538
- | 0:03-0:10 | **PROBLEM** | Agitate the pain point |
4539
- | 0:10-0:40 | **SOLUTION** | Rapid value delivery |
4540
- | 0:40-0:55 | **PROOF** | Show, don't tell |
4541
- | 0:55-0:60 | **CTA** | Soft ask (Follow/Link in bio) |
4680
+ 1. **Provide context** - More input = better presentations
4681
+ 2. **Use branding** - Extract and apply brand consistency
4682
+ 3. **Review content** - AI-generated content should be reviewed
4683
+ 4. **Export for sharing** - Use \`--output\` to create shareable files
4684
+ 5. **Iterate** - Regenerate specific slides if needed
4542
4685
 
4543
- ### CTA Best Practices
4686
+ ---
4544
4687
 
4545
- | Do | Don't |
4546
- |----|-------|
4547
- | "Follow for more tips" | "SMASH that like button" |
4548
- | "Link in bio" | "Click the link below" |
4549
- | "Save this for later" | "Subscribe to my channel" |
4688
+ ## Troubleshooting
4550
4689
 
4551
- ---
4690
+ **Authentication Issues:**
4691
+ \`\`\`bash
4692
+ # Check current user
4693
+ ${cmd2} whoami
4552
4694
 
4553
- ## 8. Quick Reference
4695
+ # Re-authenticate
4696
+ ${cmd2} logout
4697
+ ${cmd2} login
4698
+ \`\`\`
4554
4699
 
4555
- ### Platform Specs
4700
+ **Generation Failures:**
4701
+ - Ensure input is clear and has enough context
4702
+ - Try different styles or templates
4703
+ - Check API status and quotas
4556
4704
 
4557
- | Platform | Ratio | Resolution | Duration | FPS |
4558
- |----------|-------|------------|----------|-----|
4559
- | TikTok | 9:16 | 1080x1920 | 15-60s | 30 |
4560
- | Reels | 9:16 | 1080x1920 | 15-90s | 30 |
4561
- | Shorts | 9:16 | 1080x1920 | 15-60s | 30 |
4562
- | YouTube | 16:9 | 1920x1080 | any | 30/60 |
4705
+ **Export Issues:**
4706
+ - Verify output format is supported
4707
+ - Check file permissions in output directory
4708
+ - Ensure presentation ID is correct
4563
4709
 
4564
- ### Timing (30fps)
4710
+ ---
4565
4711
 
4566
- | Action | Frames | Seconds |
4567
- |--------|--------|---------|
4568
- | Hook impact | 0-3 | 0-0.1s |
4569
- | Text hold min | 45 | 1.5s |
4570
- | Scene change max | 90 | 3s |
4571
- | Ideal scene | 60 | 2s |
4712
+ ## Examples
4572
4713
 
4573
- ### Self-Check
4714
+ **Quick pitch deck:**
4715
+ \`\`\`bash
4716
+ ${cmd2} create "SaaS analytics platform for e-commerce" --slides 8 --style professional
4717
+ \`\`\`
4574
4718
 
4575
- - [ ] Text in FRAME 1 (no millennial pause)
4576
- - [ ] Visual change every 2-3 seconds
4577
- - [ ] All content within safe zones
4578
- - [ ] Captions at bottom: 350px+
4579
- - [ ] No content in bottom 20% or right 120px
4580
- - [ ] Lo-fi aesthetic (not TV ad)
4581
- - [ ] CTA is soft, not desperate
4582
- - [ ] Duration 15-60 seconds
4583
- `
4584
- }
4719
+ **From product brief:**
4720
+ \`\`\`bash
4721
+ ${cmd2} create --file brief.md --branding acme --output pitch.zip
4722
+ \`\`\`
4723
+
4724
+ **Research presentation:**
4725
+ \`\`\`bash
4726
+ cat research-notes.txt | ${cmd2} create --slides 15 --style minimal
4727
+ \`\`\`
4728
+ `;
4729
+ }
4730
+
4731
+ // src/commands/skill/installer.ts
4732
+ import { mkdirSync, writeFileSync, existsSync as existsSync2, rmSync } from "fs";
4733
+ import { join, resolve as resolve4, relative } from "path";
4734
+ import { homedir } from "os";
4735
+
4736
+ // src/commands/skill/editors.ts
4737
+ var SUPPORTED_EDITORS = [
4738
+ { name: "Claude Code", dir: ".claude" },
4739
+ { name: "Cursor", dir: ".cursor" },
4740
+ { name: "Codex", dir: ".codex" },
4741
+ { name: "OpenCode", dir: ".opencode" },
4742
+ { name: "Windsurf", dir: ".windsurf" },
4743
+ { name: "Agent", dir: ".agent" }
4585
4744
  ];
4586
4745
 
4587
4746
  // src/commands/skill/installer.ts
@@ -4594,27 +4753,10 @@ function validatePath(basePath, targetPath) {
4594
4753
  }
4595
4754
  return resolvedTarget;
4596
4755
  }
4597
- function getRuleFiles() {
4598
- const rules = [];
4599
- for (const rule of VIDEO_RULE_CONTENTS) {
4600
- rules.push({
4601
- path: `rules/video/${rule.filename}`,
4602
- content: rule.content
4603
- });
4604
- }
4605
- return rules;
4606
- }
4607
4756
  function installSkillToPath(skillPath, content) {
4608
4757
  const skillFile = join(skillPath, "SKILL.md");
4609
4758
  mkdirSync(skillPath, { recursive: true });
4610
4759
  writeFileSync(skillFile, content, "utf-8");
4611
- const rules = getRuleFiles();
4612
- for (const rule of rules) {
4613
- const rulePath = join(skillPath, rule.path);
4614
- const ruleDir = join(skillPath, rule.path.split("/").slice(0, -1).join("/"));
4615
- mkdirSync(ruleDir, { recursive: true });
4616
- writeFileSync(rulePath, rule.content, "utf-8");
4617
- }
4618
4760
  }
4619
4761
  function installSkill(skillName, content, options = {}) {
4620
4762
  const result = {
@@ -4678,67 +4820,133 @@ function getSupportedEditorNames() {
4678
4820
  }
4679
4821
 
4680
4822
  // src/commands/skill/index.ts
4681
- var skillCommand = new Command14("skill").description(`Manage ${brand.displayName} skill for AI coding assistants`).addHelpText(
4823
+ var SKILL_TYPES = ["main", "video", "presentation"];
4824
+ var skillContext = {
4825
+ name: brand.name,
4826
+ cmd: brand.commands[0],
4827
+ displayName: brand.displayName
4828
+ };
4829
+ var skillCommand = new Command14("skill").description(`Manage ${brand.displayName} skills for AI coding assistants`).addHelpText(
4682
4830
  "after",
4683
4831
  `
4832
+ ${chalk12.bold("Skill Types:")}
4833
+ ${chalk12.cyan("main")} Main CLI skill with all capabilities (TTS, music, images, videos, presentations)
4834
+ ${chalk12.cyan("video")} Detailed video creation workflow with Remotion/R3F patterns
4835
+ ${chalk12.cyan("presentation")} Detailed presentation creation workflow
4836
+
4684
4837
  ${chalk12.bold("Examples:")}
4685
- ${chalk12.gray("# Install skill for all detected editors")}
4686
- $ ${brand.name} skill install
4838
+ ${chalk12.gray("# Install main CLI skill (comprehensive overview)")}
4839
+ $ ${brand.commands[0]} skill install main
4840
+
4841
+ ${chalk12.gray("# Install video skill")}
4842
+ $ ${brand.commands[0]} skill install video
4843
+
4844
+ ${chalk12.gray("# Install presentation skill")}
4845
+ $ ${brand.commands[0]} skill install presentation
4846
+
4847
+ ${chalk12.gray("# Install all skills")}
4848
+ $ ${brand.commands[0]} skill install
4687
4849
 
4688
4850
  ${chalk12.gray("# Install to specific directory")}
4689
- $ ${brand.name} skill install --dir ~/.claude
4851
+ $ ${brand.commands[0]} skill install main --dir ~/.claude
4690
4852
 
4691
4853
  ${chalk12.gray("# Show skill content")}
4692
- $ ${brand.name} skill show
4854
+ $ ${brand.commands[0]} skill show main
4693
4855
  `
4694
4856
  );
4695
- skillCommand.command("install").description(`Install the ${brand.displayName} skill for AI coding assistants`).option("-d, --dir <path>", "Install to specific directory").option("-g, --global", "Install globally (to home directory)", true).option("-l, --local", "Install locally (to current directory)").option("-f, --force", "Overwrite existing skill files").action(async (options) => {
4696
- const skillContent = generateSkillContent(brand);
4697
- const result = installSkill(brand.name, skillContent, {
4698
- dir: options.dir,
4699
- local: options.local,
4700
- force: options.force
4701
- });
4702
- console.log();
4703
- if (result.installed.length > 0) {
4704
- success("Skill installed successfully");
4705
- console.log();
4706
- keyValue("Installed to", result.installed.join(", "));
4857
+ skillCommand.command("install").description(`Install ${brand.displayName} skills for AI coding assistants`).argument("[type]", "Skill type: main, video, presentation, or omit for all").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) => {
4858
+ const skillsToInstall = [];
4859
+ if (!type || type === "main") {
4860
+ skillsToInstall.push({
4861
+ name: brand.name,
4862
+ content: generateMainSkillContent(skillContext)
4863
+ });
4707
4864
  }
4708
- if (result.skipped.length > 0) {
4709
- console.log();
4710
- info(`Skipped (already exists): ${result.skipped.join(", ")}`);
4711
- console.log(chalk12.gray(" Use --force to overwrite"));
4865
+ if (!type || type === "video") {
4866
+ skillsToInstall.push({
4867
+ name: `${brand.name}-video`,
4868
+ content: generateVideoSkillContent(skillContext)
4869
+ });
4712
4870
  }
4713
- if (result.errors.length > 0) {
4714
- console.log();
4715
- for (const err of result.errors) {
4716
- error(err);
4717
- }
4871
+ if (!type || type === "presentation") {
4872
+ skillsToInstall.push({
4873
+ name: `${brand.name}-presentation`,
4874
+ content: generatePresentationSkillContent(skillContext)
4875
+ });
4718
4876
  }
4719
- if (result.installed.length === 0 && result.skipped.length === 0 && result.errors.length === 0) {
4720
- info("No supported AI coding assistants detected.");
4721
- console.log();
4722
- console.log(chalk12.gray("Supported editors: " + getSupportedEditorNames().join(", ")));
4723
- console.log(chalk12.gray("Use --dir <path> to install to a specific directory"));
4877
+ if (type && !SKILL_TYPES.includes(type)) {
4878
+ error(`Invalid skill type: ${type}. Must be one of: ${SKILL_TYPES.join(", ")}`);
4879
+ process.exit(1);
4724
4880
  }
4725
4881
  console.log();
4882
+ for (const skill of skillsToInstall) {
4883
+ info(`Installing ${skill.name}...`);
4884
+ const result = installSkill(skill.name, skill.content, {
4885
+ dir: options.dir,
4886
+ local: options.local,
4887
+ force: options.force
4888
+ });
4889
+ if (result.installed.length > 0) {
4890
+ success(`${skill.name} installed successfully`);
4891
+ keyValue(" Installed to", result.installed.join(", "));
4892
+ }
4893
+ if (result.skipped.length > 0) {
4894
+ info(` Skipped (already exists): ${result.skipped.join(", ")}`);
4895
+ console.log(chalk12.gray(" Use --force to overwrite"));
4896
+ }
4897
+ if (result.errors.length > 0) {
4898
+ for (const err of result.errors) {
4899
+ error(` ${err}`);
4900
+ }
4901
+ }
4902
+ if (result.installed.length === 0 && result.skipped.length === 0 && result.errors.length === 0) {
4903
+ info(" No supported AI coding assistants detected");
4904
+ console.log(chalk12.gray(" Supported editors: " + getSupportedEditorNames().join(", ")));
4905
+ console.log(chalk12.gray(" Use --dir <path> to install to a specific directory"));
4906
+ }
4907
+ console.log();
4908
+ }
4726
4909
  });
4727
- skillCommand.command("show").description("Display the skill content").action(() => {
4728
- console.log(generateSkillContent(brand));
4729
- });
4730
- skillCommand.command("uninstall").description(`Remove the ${brand.displayName} skill from AI coding assistants`).option("-g, --global", "Uninstall globally (from home directory)", true).option("-l, --local", "Uninstall locally (from current directory)").action(async (options) => {
4731
- const result = uninstallSkill(brand.name, { local: options.local });
4732
- console.log();
4733
- if (result.removed.length > 0) {
4734
- success("Skill uninstalled");
4735
- keyValue("Removed from", result.removed.join(", "));
4910
+ skillCommand.command("show").description("Display skill content").argument("[type]", "Skill type: main, video, or presentation (default: main)").action((type = "main") => {
4911
+ if (type === "main") {
4912
+ console.log(generateMainSkillContent(skillContext));
4913
+ } else if (type === "video") {
4914
+ console.log(generateVideoSkillContent(skillContext));
4915
+ } else if (type === "presentation") {
4916
+ console.log(generatePresentationSkillContent(skillContext));
4736
4917
  } else {
4737
- info("No installed skills found");
4918
+ error(`Invalid skill type: ${type}. Must be one of: ${SKILL_TYPES.join(", ")}`);
4919
+ process.exit(1);
4920
+ }
4921
+ });
4922
+ skillCommand.command("uninstall").description(`Remove ${brand.displayName} skills from AI coding assistants`).argument("[type]", "Skill type: main, video, presentation, or omit for all").option("-g, --global", "Uninstall globally (from home directory)", true).option("-l, --local", "Uninstall locally (from current directory)").action(async (type, options) => {
4923
+ const skillsToRemove = [];
4924
+ if (!type || type === "main") {
4925
+ skillsToRemove.push(brand.name);
4926
+ }
4927
+ if (!type || type === "video") {
4928
+ skillsToRemove.push(`${brand.name}-video`);
4929
+ }
4930
+ if (!type || type === "presentation") {
4931
+ skillsToRemove.push(`${brand.name}-presentation`);
4932
+ }
4933
+ if (type && !SKILL_TYPES.includes(type)) {
4934
+ error(`Invalid skill type: ${type}. Must be one of: ${SKILL_TYPES.join(", ")}`);
4935
+ process.exit(1);
4738
4936
  }
4739
- if (result.errors.length > 0) {
4740
- for (const err of result.errors) {
4741
- warn(`Failed to remove: ${err}`);
4937
+ console.log();
4938
+ for (const skillName of skillsToRemove) {
4939
+ const result = uninstallSkill(skillName, { local: options.local });
4940
+ if (result.removed.length > 0) {
4941
+ success(`${skillName} uninstalled`);
4942
+ keyValue(" Removed from", result.removed.join(", "));
4943
+ } else {
4944
+ info(` ${skillName} not found`);
4945
+ }
4946
+ if (result.errors.length > 0) {
4947
+ for (const err of result.errors) {
4948
+ warn(` Failed to remove: ${err}`);
4949
+ }
4742
4950
  }
4743
4951
  }
4744
4952
  console.log();
@@ -5000,7 +5208,7 @@ async function downloadFile2(url, outputPath) {
5000
5208
  const buffer = await response.arrayBuffer();
5001
5209
  await writeFile4(outputPath, Buffer.from(buffer));
5002
5210
  }
5003
- var mixCommand = new Command17("create").description("Mix audio tracks into a video").requiredOption("--video <url>", "Input video file/URL").option("--music <url>", "Background music file/URL").option("--voice <url>", "Voiceover file/URL").option("--music-volume <percent>", "Music volume 0-100", "50").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) => {
5211
+ var mixCommand = new Command17("create").description("Mix audio tracks into a video (music will loop to match video duration)").requiredOption("--video <url>", "Input video file/URL").option("--music <url>", "Background music file/URL (will loop if shorter than video)").option("--voice <url>", "Voiceover file/URL").option("--music-volume <percent>", "Music volume 0-100 (default: 30, recommended for mix with voice)", "30").option("--voice-volume <percent>", "Voice volume 0-100", "100").option("-o, --output <path>", "Output file path").option("--no-wait", "Do not wait for completion").option("-f, --format <format>", "Output format: human, json, quiet", "human").action(async (options) => {
5004
5212
  if (!options.music && !options.voice) {
5005
5213
  error("At least one of --music or --voice must be provided");
5006
5214
  process.exit(EXIT_CODES.INVALID_INPUT);
@@ -5169,9 +5377,10 @@ init_output();
5169
5377
  init_types();
5170
5378
  import { Command as Command19 } from "commander";
5171
5379
  import ora12 from "ora";
5172
- import { mkdir, writeFile as writeFile5, readFile as readFile2, access, rm } from "fs/promises";
5380
+ import { mkdir, writeFile as writeFile5, readFile as readFile2, access, rm, cp } from "fs/promises";
5173
5381
  import { join as join2, resolve as resolve5 } from "path";
5174
5382
  import { execSync, spawn } from "child_process";
5383
+ import ffmpegPath from "ffmpeg-static";
5175
5384
  var DEFAULT_TEMPLATE = "inizio-inc/remotion-composition";
5176
5385
  var DEFAULT_FPS = 30;
5177
5386
  function parseScriptIntoSections(script) {
@@ -5310,7 +5519,7 @@ function getExtension(url) {
5310
5519
  }
5311
5520
  return "jpg";
5312
5521
  }
5313
- var createCommand3 = new Command19("create").description("Create video assets (voiceover per scene, music, images)").option("-s, --script <text>", "Narration script (legacy single-script mode)").option("--script-file <path>", "Path to script file (legacy) or scenes JSON").option("-t, --topic <text>", "Topic for image search").option("-v, --voice <name>", "TTS voice (Kore, Puck, Rachel, alloy)", "Kore").option("-m, --music-prompt <text>", "Music description").option("-n, --num-images <number>", "Number of images to search/download", "5").option("-o, --output <dir>", "Output directory", "./public").option("-f, --format <format>", "Output format: human, json, quiet", "human").action(async (options) => {
5522
+ var createCommand2 = new Command19("create").description("Create video assets (voiceover per scene, music, images)").option("-s, --script <text>", "Narration script (legacy single-script mode)").option("--script-file <path>", "Path to script file (legacy) or scenes JSON").option("-t, --topic <text>", "Topic for image search").option("-v, --voice <name>", "TTS voice (Kore, Puck, Rachel, alloy)", "Kore").option("-m, --music-prompt <text>", "Music description").option("-n, --num-images <number>", "Number of images to search/download", "5").option("-o, --output <dir>", "Output directory", "./public").option("-f, --format <format>", "Output format: human, json, quiet", "human").action(async (options) => {
5314
5523
  const format = options.format;
5315
5524
  const spinner = format === "human" ? ora12("Initializing...").start() : null;
5316
5525
  try {
@@ -5362,7 +5571,10 @@ var createCommand3 = new Command19("create").description("Create video assets (v
5362
5571
  if (spinner) spinner.text = `[${scene.name}] Generating speech...`;
5363
5572
  const ttsResult = await generateSpeech({
5364
5573
  text: scene.script,
5365
- options: { voice }
5574
+ options: {
5575
+ voice,
5576
+ voiceSettings: scenesInput.voiceSettings
5577
+ }
5366
5578
  });
5367
5579
  const audioPath = join2(audioDir, `${filename}.${ttsResult.format}`);
5368
5580
  await writeFile5(audioPath, ttsResult.audioData);
@@ -5417,7 +5629,7 @@ var createCommand3 = new Command19("create").description("Create video assets (v
5417
5629
  try {
5418
5630
  const videoResults = await searchVideos({
5419
5631
  query: scene.videoQuery,
5420
- options: { maxResults: 1 }
5632
+ options: { maxResults: 1, license: "free" }
5421
5633
  });
5422
5634
  const vids = videoResults.data.results.flatMap((r) => r.results);
5423
5635
  totalCost += videoResults.data.totalCost;
@@ -5502,7 +5714,12 @@ var createCommand3 = new Command19("create").description("Create video assets (v
5502
5714
  spinner?.start();
5503
5715
  }
5504
5716
  }
5505
- const musicDuration = Math.min(30, Math.ceil(totalDuration) + 5);
5717
+ const musicDuration = Math.min(30, Math.ceil(totalDuration));
5718
+ console.log(`[Music Generation] Requesting music:`, {
5719
+ prompt: musicPrompt,
5720
+ requestedDuration: musicDuration,
5721
+ totalAudioDuration: totalDuration
5722
+ });
5506
5723
  if (spinner) spinner.text = "Generating music...";
5507
5724
  let musicResult = await generateMusic({
5508
5725
  prompt: musicPrompt,
@@ -5526,15 +5743,28 @@ var createCommand3 = new Command19("create").description("Create video assets (v
5526
5743
  await downloadFile3(musicResult.audioUrl, musicPath);
5527
5744
  }
5528
5745
  totalCost += musicResult.cost || 0;
5746
+ const actualMusicDuration = musicResult.duration || musicDuration;
5747
+ console.log(`[Music Generation] Received music:`, {
5748
+ requestedDuration: musicDuration,
5749
+ returnedDuration: musicResult.duration,
5750
+ actualUsedDuration: actualMusicDuration,
5751
+ totalAudioDuration: totalDuration,
5752
+ difference: actualMusicDuration - totalDuration,
5753
+ audioUrl: musicResult.audioUrl?.substring(0, 50) + "..."
5754
+ });
5529
5755
  const musicInfo = {
5530
5756
  path: "audio/music.mp3",
5531
- duration: musicResult.duration || musicDuration,
5757
+ duration: actualMusicDuration,
5532
5758
  prompt: musicPrompt,
5533
5759
  cost: musicResult.cost || 0
5534
5760
  };
5535
5761
  if (format === "human") {
5536
5762
  spinner?.stop();
5537
5763
  success(`Music: ${musicPath} (${musicInfo.duration}s)`);
5764
+ if (actualMusicDuration < totalDuration) {
5765
+ warn(`Music duration (${actualMusicDuration.toFixed(1)}s) is shorter than video duration (${totalDuration.toFixed(1)}s).`);
5766
+ warn(`Consider using audio looping or extending music in Remotion.`);
5767
+ }
5538
5768
  spinner?.start();
5539
5769
  }
5540
5770
  if (spinner) spinner.text = "Writing manifest...";
@@ -5582,7 +5812,7 @@ var createCommand3 = new Command19("create").description("Create video assets (v
5582
5812
  process.exit(EXIT_CODES.GENERAL_ERROR);
5583
5813
  }
5584
5814
  });
5585
- var searchCommand2 = new Command19("search").description("Search for stock videos").argument("<query>", "Search query").option("-n, --max-results <count>", "Maximum number of results", "10").option("-o, --orientation <type>", "Video orientation: landscape, portrait, square, any", "any").option("-l, --license <type>", "License type: free, premium, any", "any").option("-f, --format <format>", "Output format: human, json, quiet", "human").action(async (query, options) => {
5815
+ var searchCommand2 = new Command19("search").description("Search for stock videos").argument("<query>", "Search query").option("-n, --max-results <count>", "Maximum number of results", "10").option("-o, --orientation <type>", "Video orientation: landscape, portrait, square, any", "any").option("-l, --license <type>", "License type: free, premium, any", "free").option("-f, --format <format>", "Output format: human, json, quiet", "human").action(async (query, options) => {
5586
5816
  const { maxResults, orientation, license, format } = options;
5587
5817
  const spinner = format === "human" ? ora12("Searching for videos...").start() : null;
5588
5818
  try {
@@ -5751,10 +5981,147 @@ var initCommand = new Command19("init").description("Create a new Remotion video
5751
5981
  process.exit(EXIT_CODES.GENERAL_ERROR);
5752
5982
  }
5753
5983
  });
5754
- var videoCommand = new Command19("video").description("Video asset generation commands").addCommand(initCommand).addCommand(createCommand3).addCommand(searchCommand2);
5984
+ function getFfmpegPath() {
5985
+ if (!ffmpegPath) {
5986
+ throw new Error("ffmpeg-static binary not found. Try reinstalling the CLI.");
5987
+ }
5988
+ return ffmpegPath;
5989
+ }
5990
+ 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) => {
5991
+ const format = options.json ? "json" : options.quiet ? "quiet" : "human";
5992
+ const spinner = format === "human" ? ora12("Processing...").start() : null;
5993
+ try {
5994
+ let ffmpeg;
5995
+ try {
5996
+ ffmpeg = getFfmpegPath();
5997
+ } catch (err) {
5998
+ spinner?.stop();
5999
+ error(err instanceof Error ? err.message : "ffmpeg not available");
6000
+ process.exit(EXIT_CODES.GENERAL_ERROR);
6001
+ }
6002
+ const videoFullPath = resolve5(process.cwd(), videoPath);
6003
+ try {
6004
+ await access(videoFullPath);
6005
+ } catch {
6006
+ spinner?.stop();
6007
+ error(`Video file not found: ${videoPath}`);
6008
+ process.exit(EXIT_CODES.INVALID_INPUT);
6009
+ }
6010
+ const frameNum = parseInt(options.frame, 10);
6011
+ if (isNaN(frameNum) || frameNum < 0) {
6012
+ spinner?.stop();
6013
+ error("Invalid frame number. Must be a non-negative integer.");
6014
+ process.exit(EXIT_CODES.INVALID_INPUT);
6015
+ }
6016
+ let thumbnailPath = options.image;
6017
+ let tempThumbnail = false;
6018
+ if (!thumbnailPath) {
6019
+ const tempDir = join2(process.cwd(), ".tmp-thumbnail");
6020
+ await mkdir(tempDir, { recursive: true });
6021
+ thumbnailPath = join2(tempDir, "thumb.png");
6022
+ tempThumbnail = true;
6023
+ if (options.composition) {
6024
+ if (spinner) spinner.text = `Extracting frame ${frameNum} from ${options.composition}...`;
6025
+ const args = [
6026
+ "exec",
6027
+ "remotion",
6028
+ "still",
6029
+ options.composition,
6030
+ thumbnailPath,
6031
+ `--frame=${frameNum}`
6032
+ ];
6033
+ try {
6034
+ execSync(`pnpm ${args.join(" ")}`, {
6035
+ stdio: "pipe",
6036
+ cwd: process.cwd()
6037
+ });
6038
+ } catch (err) {
6039
+ spinner?.stop();
6040
+ error(`Failed to extract frame from composition: ${err instanceof Error ? err.message : "Unknown error"}`);
6041
+ process.exit(EXIT_CODES.GENERAL_ERROR);
6042
+ }
6043
+ } else {
6044
+ if (spinner) spinner.text = `Extracting frame ${frameNum} from video...`;
6045
+ try {
6046
+ execSync(
6047
+ `"${ffmpeg}" -y -i "${videoFullPath}" -vf "select=eq(n\\,${frameNum})" -vframes 1 "${thumbnailPath}"`,
6048
+ { stdio: "pipe" }
6049
+ );
6050
+ } catch (err) {
6051
+ spinner?.stop();
6052
+ error(`Failed to extract frame from video: ${err instanceof Error ? err.message : "Unknown error"}`);
6053
+ process.exit(EXIT_CODES.GENERAL_ERROR);
6054
+ }
6055
+ }
6056
+ } else {
6057
+ thumbnailPath = resolve5(process.cwd(), thumbnailPath);
6058
+ try {
6059
+ await access(thumbnailPath);
6060
+ } catch {
6061
+ spinner?.stop();
6062
+ error(`Thumbnail image not found: ${options.image}`);
6063
+ process.exit(EXIT_CODES.INVALID_INPUT);
6064
+ }
6065
+ }
6066
+ const outputPath = options.output ? resolve5(process.cwd(), options.output) : videoFullPath;
6067
+ const needsTempOutput = outputPath === videoFullPath;
6068
+ const tempOutput = needsTempOutput ? videoFullPath.replace(/\.mp4$/, ".thumb-temp.mp4") : outputPath;
6069
+ if (spinner) spinner.text = "Embedding thumbnail into video...";
6070
+ try {
6071
+ execSync(
6072
+ `"${ffmpeg}" -y -i "${videoFullPath}" -i "${thumbnailPath}" -map 0 -map 1 -c copy -disposition:v:1 attached_pic "${tempOutput}"`,
6073
+ { stdio: "pipe" }
6074
+ );
6075
+ if (needsTempOutput) {
6076
+ await rm(videoFullPath);
6077
+ await cp(tempOutput, videoFullPath);
6078
+ await rm(tempOutput);
6079
+ }
6080
+ } catch (err) {
6081
+ spinner?.stop();
6082
+ error(`Failed to embed thumbnail: ${err instanceof Error ? err.message : "Unknown error"}`);
6083
+ process.exit(EXIT_CODES.GENERAL_ERROR);
6084
+ }
6085
+ if (tempThumbnail) {
6086
+ try {
6087
+ await rm(join2(process.cwd(), ".tmp-thumbnail"), { recursive: true });
6088
+ } catch {
6089
+ }
6090
+ }
6091
+ spinner?.stop();
6092
+ const finalOutput = options.output || videoPath;
6093
+ if (format === "json") {
6094
+ printJson({
6095
+ video: finalOutput,
6096
+ thumbnail: options.image || `frame ${frameNum}`,
6097
+ composition: options.composition || null
6098
+ });
6099
+ return;
6100
+ }
6101
+ if (format === "quiet") {
6102
+ console.log(resolve5(process.cwd(), finalOutput));
6103
+ return;
6104
+ }
6105
+ console.log();
6106
+ success(`Thumbnail embedded: ${finalOutput}`);
6107
+ if (options.image) {
6108
+ keyValue("Thumbnail", options.image);
6109
+ } else if (options.composition) {
6110
+ keyValue("Source", `${options.composition} frame ${frameNum}`);
6111
+ } else {
6112
+ keyValue("Source", `Video frame ${frameNum}`);
6113
+ }
6114
+ console.log();
6115
+ } catch (err) {
6116
+ spinner?.stop();
6117
+ error(err instanceof Error ? err.message : "Failed to process thumbnail");
6118
+ process.exit(EXIT_CODES.GENERAL_ERROR);
6119
+ }
6120
+ });
6121
+ var videoCommand = new Command19("video").description("Video asset generation commands").addCommand(initCommand).addCommand(createCommand2).addCommand(searchCommand2).addCommand(thumbnailCommand);
5755
6122
 
5756
6123
  // src/index.ts
5757
- var VERSION = "0.1.7";
6124
+ var VERSION = "0.1.9";
5758
6125
  var program = new Command20();
5759
6126
  var cmdName = brand.commands[0];
5760
6127
  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({