@conceptcraft/mindframes 0.1.7 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,569 +3140,422 @@ var whoamiCommand = new Command13("whoami").description("Show current user and t
3129
3140
 
3130
3141
  // src/commands/skill/index.ts
3131
3142
  init_output();
3143
+ init_brand();
3132
3144
  import { Command as Command14 } from "commander";
3133
3145
  import chalk12 from "chalk";
3134
3146
 
3135
- // src/commands/skill/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
3156
-
3157
- \`\`\`bash
3158
- npm install -g ${ctx.pkg}
3159
- ${ctx.cmd} login # Authenticate (opens browser)
3160
- ${ctx.cmd} whoami # Verify auth
3161
- \`\`\`
3147
+ // src/commands/skill/rules/video/content.ts
3148
+ var VIDEO_RULE_CONTENTS = [
3149
+ {
3150
+ filename: "video-creation-guide.md",
3151
+ content: `# Video Creation Guide
3162
3152
 
3163
- ### Authentication
3153
+ ### Related Skills
3164
3154
 
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
- };
3155
+ Use these installed skills for implementation details:
3156
+ - \`remotion-best-practices\` \u2014 Remotion patterns and API
3157
+ - \`threejs-*\` skills \u2014 for R3F/WebGL (particles, 3D elements)
3168
3158
 
3169
- // src/commands/skill/sections/workflow.ts
3170
- var workflow = {
3171
- title: "Core Workflow",
3172
- render: (ctx) => `## Core Workflow
3159
+ ---
3173
3160
 
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
- };
3161
+ ## Core Rules
3178
3162
 
3179
- // src/commands/skill/sections/create-command.ts
3180
- var createCommand2 = {
3181
- title: "Create Command",
3182
- render: (ctx) => `## Commands
3163
+ Your task is not "making slideshows" \u2014 you are **simulating a real interface** that obeys cinematic physics.
3183
3164
 
3184
- ### Create Presentation
3165
+ ### Hard Constraints
3185
3166
 
3186
- Context is **required**. Provide it via one of these methods:
3167
+ 1. **No scene > 8 seconds** without cut or major action
3168
+ 2. **No static pixels** \u2014 everything breathes, drifts, pulses
3169
+ 3. **No linear interpolation** \u2014 use \`spring()\` physics
3170
+ 4. **Scene overlap 15-20 frames** \u2014 no hard cuts
3171
+ 5. **60 FPS mandatory** \u2014 30fps looks choppy
3172
+ 6. **No screenshots for UI** \u2014 rebuild in React/CSS
3187
3173
 
3188
- \`\`\`bash
3189
- # Upload files (PDFs, PPTX, images, docs)
3190
- ${ctx.cmd} create "Product Overview" --file ./deck.pptx --file ./logo.png
3174
+ ---
3191
3175
 
3192
- # Direct text context
3193
- ${ctx.cmd} create "Topic Title" --context "Key points, data, facts..."
3176
+ ## Code Organization
3194
3177
 
3195
- # From a text file
3196
- ${ctx.cmd} create "Topic Title" --context-file ./notes.md
3178
+ - Create separate files: \`Button.tsx\`, \`Window.tsx\`, \`Cursor.tsx\`
3179
+ - Use Zod schemas for props validation
3180
+ - Extract animation configs to constants
3197
3181
 
3198
- # Pipe content (auto-detected)
3199
- cat README.md | ${ctx.cmd} create "Project Overview"
3182
+ \`\`\`tsx
3183
+ import { spring, interpolate, useCurrentFrame, useVideoConfig } from 'remotion';
3200
3184
 
3201
- # From URLs (scraped automatically)
3202
- ${ctx.cmd} create "Competitor Analysis" --sources https://example.com/report
3185
+ // ALWAYS use spring for element entrances
3186
+ // NEVER use magic numbers
3187
+ \`\`\`
3203
3188
 
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
- };
3189
+ ---
3210
3190
 
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
- };
3191
+ ## Aesthetics (Linear/Stripe Style)
3230
3192
 
3231
- // src/commands/skill/sections/other-commands.ts
3232
- var otherCommands = {
3233
- title: "Other Commands",
3234
- render: (ctx) => `### Other Commands
3193
+ \`\`\`css
3194
+ /* Shadows - soft, expensive */
3195
+ box-shadow: 0 20px 50px -12px rgba(0,0,0,0.5);
3235
3196
 
3236
- \`\`\`bash
3237
- # Check authentication
3238
- ${ctx.cmd} whoami
3197
+ /* Borders - thin, barely visible */
3198
+ border: 1px solid rgba(255,255,255,0.1);
3199
+ \`\`\`
3239
3200
 
3240
- # List presentations
3241
- ${ctx.cmd} list
3242
- ${ctx.cmd} list --format json
3201
+ - Fonts: Inter or SF Pro
3202
+ - Never pure \`#000000\` \u2014 use \`#050505\`
3203
+ - Never pure \`#FFFFFF\` \u2014 use \`#F0F0F0\`
3243
3204
 
3244
- # Get presentation details
3245
- ${ctx.cmd} get <id-or-slug>
3205
+ ---
3246
3206
 
3247
- # Export to ZIP
3248
- ${ctx.cmd} export <id-or-slug> -o presentation.zip
3207
+ ## Self-Check Before Render
3249
3208
 
3250
- # Import presentation
3251
- ${ctx.cmd} import ./presentation.zip
3209
+ - [ ] Camera rig wraps entire scene with drift/zoom
3210
+ - [ ] Every UI element uses 2.5D rotation entrance
3211
+ - [ ] Cursor moves in curves with overshoot
3212
+ - [ ] Lists/grids stagger (never appear all at once)
3213
+ - [ ] Background has moving orbs + vignette + noise
3214
+ - [ ] Something is moving on EVERY frame
3215
+ - [ ] Scene transitions overlap (no hard cuts)
3252
3216
 
3253
- # Manage branding
3254
- ${ctx.cmd} branding list
3255
- ${ctx.cmd} branding extract https://company.com
3217
+ **If your video looks like PowerPoint with voiceover \u2014 START OVER.**
3218
+ `
3219
+ },
3220
+ {
3221
+ filename: "animation-physics.md",
3222
+ content: `# Animation Physics
3256
3223
 
3257
- # Install/manage this skill
3258
- ${ctx.cmd} skill install
3259
- ${ctx.cmd} skill show
3260
- \`\`\``
3261
- };
3224
+ ## Spring Configurations
3262
3225
 
3263
- // src/commands/skill/sections/examples.ts
3264
- var examples = {
3265
- title: "Examples",
3266
- render: (ctx) => `## Examples
3226
+ ### Heavy UI (Modals, Sidebars)
3227
+ \`\`\`tsx
3228
+ config: { mass: 1, stiffness: 100, damping: 15 }
3229
+ \`\`\`
3267
3230
 
3268
- ### Present a Codebase Feature
3231
+ ### Light UI (Tooltips, Badges)
3232
+ \`\`\`tsx
3233
+ config: { mass: 0.6, stiffness: 180, damping: 12 }
3234
+ \`\`\`
3269
3235
 
3270
- \`\`\`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
3236
+ ### Standard (Snappy)
3237
+ \`\`\`tsx
3238
+ config: { mass: 1, damping: 15, stiffness: 120 }
3275
3239
  \`\`\`
3276
3240
 
3277
- ### Technical Documentation with Diagrams
3241
+ ---
3278
3242
 
3279
- \`\`\`bash
3280
- ${ctx.cmd} create "API Reference" \\
3281
- --file ./docs/api.md \\
3282
- --file ./diagrams/architecture.png \\
3283
- --mode best --amount detailed \\
3284
- --goal inform
3285
- \`\`\`
3243
+ ## Staggering
3286
3244
 
3287
- ### Quick Project Overview
3245
+ **NEVER show a list all at once.**
3288
3246
 
3289
- \`\`\`bash
3290
- cat README.md package.json | ${ctx.cmd} create "Project Introduction" \\
3291
- -m instant --slides 5
3292
- \`\`\`
3247
+ \`\`\`tsx
3248
+ const STAGGER_FRAMES = 3; // 3-5 frames between items
3293
3249
 
3294
- ### Sales Deck from Existing Presentation
3250
+ {items.map((item, i) => {
3251
+ const delay = i * STAGGER_FRAMES;
3252
+ const progress = spring({
3253
+ frame: frame - delay,
3254
+ fps,
3255
+ config: { damping: 15, stiffness: 120 },
3256
+ });
3295
3257
 
3296
- \`\`\`bash
3297
- ${ctx.cmd} create "Product Demo" \\
3298
- --file ./existing-deck.pptx \\
3299
- --goal persuade \\
3300
- --audience "Enterprise buyers" \\
3301
- --tone professional
3258
+ return (
3259
+ <div style={{
3260
+ opacity: progress,
3261
+ transform: \`translateY(\${interpolate(progress, [0, 1], [20, 0])}px)\`,
3262
+ }}>
3263
+ {item}
3264
+ </div>
3265
+ );
3266
+ })}
3302
3267
  \`\`\`
3303
3268
 
3304
- ### Research Presentation
3269
+ ---
3305
3270
 
3306
- \`\`\`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
- };
3271
+ ## Cursor Movement
3314
3272
 
3315
- // src/commands/skill/sections/output.ts
3316
- var output = {
3317
- title: "Output",
3318
- render: (ctx) => `## Output
3273
+ **Cursor NEVER moves in straight lines.**
3319
3274
 
3320
- Successful creation returns:
3321
- \`\`\`
3322
- \u2713 Presentation created successfully
3275
+ \`\`\`tsx
3276
+ const progress = spring({
3277
+ frame: frame - startFrame,
3278
+ fps,
3279
+ config: { damping: 20, stiffness: 80 },
3280
+ });
3323
3281
 
3324
- Title: Authentication System
3325
- Slides: 8
3326
- Generated in: 45s \xB7 12,500 tokens
3282
+ const linearX = interpolate(progress, [0, 1], [start.x, end.x]);
3283
+ const linearY = interpolate(progress, [0, 1], [start.y, end.y]);
3327
3284
 
3328
- Open: ${ctx.url}/en/view/presentations/auth-system-v1-abc123
3285
+ // THE ARC: Parabola that peaks mid-travel
3286
+ const arcHeight = 100;
3287
+ const arcOffset = Math.sin(progress * Math.PI) * arcHeight;
3288
+ const cursorY = linearY - arcOffset;
3329
3289
  \`\`\`
3330
3290
 
3331
- For scripting, use JSON output:
3332
- \`\`\`bash
3333
- URL=$(${ctx.cmd} create "Demo" --context "..." -o json | jq -r '.viewUrl')
3334
- \`\`\``
3335
- };
3291
+ ---
3336
3292
 
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
- };
3293
+ ## Click Interaction
3349
3294
 
3350
- // src/commands/skill/sections/file-types.ts
3351
- var fileTypes = {
3352
- title: "Supported File Types",
3353
- render: (_ctx) => `## Supported File Types
3295
+ \`\`\`tsx
3296
+ // On click:
3297
+ const cursorScale = isClicking ? 0.95 : 1;
3298
+ const buttonScaleX = isClicking ? 1.02 : 1;
3299
+ const buttonScaleY = isClicking ? 0.95 : 1;
3300
+ // Release both with spring
3301
+ \`\`\`
3354
3302
 
3355
- - **Documents**: PDF, DOCX, XLSX, PPTX
3356
- - **Images**: JPEG, PNG, GIF, WebP
3357
- - **Text**: Markdown, TXT, CSV, JSON`
3358
- };
3303
+ ---
3359
3304
 
3360
- // src/commands/skill/sections/troubleshooting.ts
3361
- var troubleshooting = {
3362
- title: "Troubleshooting",
3363
- render: (ctx) => `## Troubleshooting
3305
+ ## Timing Reference
3364
3306
 
3365
- \`\`\`bash
3366
- # Check if authenticated
3367
- ${ctx.cmd} whoami
3307
+ | Action | Frames (60fps) |
3308
+ |--------|----------------|
3309
+ | Element entrance | 15-20 |
3310
+ | Stagger gap | 3-5 |
3311
+ | Hold on key info | 45-60 |
3312
+ | Scene transition | 20-30 |
3313
+ | Fast interaction | 15-20 |
3314
+ `
3315
+ },
3316
+ {
3317
+ filename: "scene-structure.md",
3318
+ content: `# Scene Structure
3368
3319
 
3369
- # Re-authenticate if needed
3370
- ${ctx.cmd} login
3320
+ ## SceneWrapper
3371
3321
 
3372
- # Debug mode
3373
- ${ctx.cmd} create "Test" --context "test" --debug
3374
- \`\`\``
3375
- };
3322
+ \`\`\`tsx
3323
+ <SceneWrapper
3324
+ durationInFrames={300}
3325
+ transitionType="slideLeft"
3326
+ cameraMotion="panRight"
3327
+ >
3328
+ <FeatureLayer />
3329
+ <CursorLayer />
3330
+ <ParticleLayer />
3331
+ </SceneWrapper>
3332
+ \`\`\`
3376
3333
 
3377
- // src/commands/skill/sections/video-creation.ts
3378
- var videoCreation = {
3379
- title: "Video Creation",
3380
- render: (ctx) => `## Video Creation
3334
+ ---
3381
3335
 
3382
- ### How to Create a Perfect Video
3336
+ ## Layer Structure (Z-Index)
3383
3337
 
3384
- Videos must feel like premium tech launches (Stripe, Apple, Linear) - **Kinetic Composition**, not slideshows.
3338
+ | Layer | Z-Index |
3339
+ |-------|---------|
3340
+ | Background orbs | 0 |
3341
+ | Vignette | 1 |
3342
+ | UI Base | 10 |
3343
+ | UI Elements | 20 |
3344
+ | Overlays | 30 |
3345
+ | Text/Captions | 40 |
3346
+ | Cursor | 50 |
3385
3347
 
3386
- **Core Philosophy:** "Nothing sits still. Everything is physics-based. Every pixel breathes."
3348
+ ---
3387
3349
 
3388
- ### Required Reading (The Motion Bible)
3350
+ ## Case Study: SaaS Task Tracker
3389
3351
 
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 |
3352
+ ### Scene 1: "The Hook" (~5s)
3398
3353
 
3399
- **Required:** \`npx skills add https://github.com/remotion-dev/skills --skill remotion-best-practices\`
3354
+ 1. Dark background (\`#0B0C10\`), grid drifting
3355
+ 2. Scattered circles magnetically attract \u2192 morph into logo
3356
+ 3. Logo expands \u2192 becomes sidebar navigation
3400
3357
 
3401
- ### Workflow
3358
+ ### Scene 2: "Micro-Interaction" (~6s)
3402
3359
 
3403
- #### Phase 1: Discovery
3360
+ 1. Modal "Create Issue" appears
3361
+ 2. Text types character by character (non-uniform speed)
3362
+ 3. \`CMD + K\` hint glows, keys animate
3363
+ 4. Cursor flies to "Save" in arc, slows on approach
3404
3364
 
3405
- Explore the project thoroughly - assets, components, branding, what makes this product/topic unique.
3365
+ ### Scene 3: "The Connection" (~5s)
3406
3366
 
3407
- #### Phase 2: Video Brief
3367
+ 1. Task card grabbed, scales 1.05, shadow deepens
3368
+ 2. Other cards spread apart
3369
+ 3. **Match Cut:** Zoom into avatar \u2192 color fills screen \u2192 becomes mobile notification background
3408
3370
 
3409
- Present a brief outline (scenes, duration, assets found) and get user approval before production.
3371
+ ---
3410
3372
 
3411
- #### Phase 3: Production
3373
+ ## Composition
3412
3374
 
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?
3375
+ \`\`\`tsx
3376
+ <AbsoluteFill>
3377
+ <MovingBackground />
3378
+ <Vignette />
3379
+ <CameraRig>
3380
+ <Sequence from={0} durationInFrames={100}>
3381
+ <Scene1 />
3382
+ </Sequence>
3383
+ <Sequence from={85} durationInFrames={150}> {/* 15 frame overlap! */}
3384
+ <Scene2 />
3385
+ </Sequence>
3386
+ </CameraRig>
3387
+ <Audio src={music} volume={0.3} />
3388
+ </AbsoluteFill>
3389
+ \`\`\`
3390
+ `
3391
+ },
3392
+ {
3393
+ filename: "scene-transitions.md",
3394
+ content: `# Scene Transitions
3419
3395
 
3420
- #### Phase 4: Render
3396
+ **No FadeIn/FadeOut.** Only contextual transitions.
3421
3397
 
3422
- Auto-render when done: \`pnpm exec remotion render FullVideo\`
3398
+ ---
3423
3399
 
3424
- Only open Studio if user asks.
3400
+ ## Types
3425
3401
 
3426
- ### Kinetic Checklist
3402
+ ### 1. Object Persistence
3403
+ Same chart transforms (data, color, scale) while UI changes around it.
3427
3404
 
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
3405
+ ### 2. Mask Reveal
3406
+ Button expands to screen size via SVG \`clipPath\`.
3436
3407
 
3437
- ### Asset Generation
3408
+ ### 3. Speed Ramps
3409
+ Scene A accelerates out, Scene B starts fast then slows.
3438
3410
 
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
- };
3411
+ ---
3453
3412
 
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
- }
3413
+ ## Match Cut Example
3482
3414
 
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";
3415
+ \`\`\`
3416
+ Scene A: Zoom into avatar
3417
+ \u2193
3418
+ Avatar color fills screen
3419
+ \u2193
3420
+ Scene B: That color IS the notification background
3421
+ \`\`\`
3487
3422
 
3488
- // 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
- ];
3423
+ ---
3497
3424
 
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
3425
+ ## Overlapping Sequences (CRITICAL)
3503
3426
 
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
3427
+ \`\`\`tsx
3428
+ <Sequence from={0} durationInFrames={100}>
3429
+ <SceneOne />
3430
+ </Sequence>
3431
+ <Sequence from={85} durationInFrames={150}> {/* 15 frames early! */}
3432
+ <SceneTwo />
3433
+ </Sequence>
3434
+ \`\`\`
3507
3435
 
3508
3436
  ---
3509
3437
 
3510
- # 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).
3438
+ ## TransitionSeries
3513
3439
 
3514
- **Core Philosophy:** "Nothing sits still. Everything is physics-based. Every pixel breathes."
3440
+ \`\`\`tsx
3441
+ import { TransitionSeries, linearTiming } from '@remotion/transitions';
3442
+ import { slide } from '@remotion/transitions/slide';
3443
+
3444
+ <TransitionSeries>
3445
+ <TransitionSeries.Sequence durationInFrames={100}>
3446
+ <SceneOne />
3447
+ </TransitionSeries.Sequence>
3448
+ <TransitionSeries.Transition
3449
+ presentation={slide({ direction: 'from-bottom' })}
3450
+ timing={linearTiming({ durationInFrames: 20 })}
3451
+ />
3452
+ <TransitionSeries.Sequence durationInFrames={150}>
3453
+ <SceneTwo />
3454
+ </TransitionSeries.Sequence>
3455
+ </TransitionSeries>
3456
+ \`\`\`
3457
+ `
3458
+ },
3459
+ {
3460
+ filename: "polish-effects.md",
3461
+ content: `# Polish Effects
3515
3462
 
3516
- 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.
3463
+ ## Reflection (Glass Glint)
3517
3464
 
3518
- ---
3465
+ Diagonal gradient sweeps every 5 seconds.
3519
3466
 
3520
- ## 1. The Global Camera Rig (The "Anti-Static" Layer)
3467
+ \`\`\`tsx
3468
+ const cycleFrame = frame % 300;
3469
+ const sweepProgress = interpolate(cycleFrame, [0, 60], [-100, 200], {
3470
+ extrapolateRight: 'clamp',
3471
+ });
3472
+ \`\`\`
3521
3473
 
3522
- Even when reading text, the screen is slowly zooming or panning.
3474
+ ---
3523
3475
 
3524
- ### Virtual Camera
3476
+ ## Background Breathing
3525
3477
 
3526
- Every scene must be wrapped in a \`CameraRig\` component:
3478
+ Background is NEVER static.
3527
3479
 
3528
3480
  \`\`\`tsx
3529
- const CameraRig: React.FC<{ children: React.ReactNode }> = ({ children }) => {
3530
- const frame = useCurrentFrame();
3531
- const { durationInFrames } = useVideoConfig();
3532
-
3533
- // The "Drift" - constant subtle movement
3534
- const scale = interpolate(frame, [0, durationInFrames], [1.0, 1.05]);
3535
- const rotation = interpolate(frame, [0, durationInFrames], [0, 0.5]);
3536
-
3537
- return (
3538
- <AbsoluteFill style={{
3539
- transform: \`scale(\${scale}) rotate(\${rotation}deg)\`,
3540
- }}>
3541
- {children}
3542
- </AbsoluteFill>
3543
- );
3544
- };
3481
+ const orb1X = Math.sin(frame / 60) * 200;
3482
+ const orb1Y = Math.cos(frame / 80) * 100;
3545
3483
  \`\`\`
3546
3484
 
3547
- ### Zoom-Through Transitions
3485
+ ---
3548
3486
 
3549
- Transitions are NOT just fades - they are camera movements:
3487
+ ## Typewriter Effect
3550
3488
 
3551
3489
  \`\`\`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
3490
+ const charIndex = Math.floor(frame / 3);
3491
+ const showCursor = Math.floor(frame / 15) % 2 === 0;
3492
+
3493
+ <span>
3494
+ {text.slice(0, charIndex)}
3495
+ {showCursor && <span>|</span>}
3496
+ </span>
3555
3497
  \`\`\`
3556
3498
 
3557
3499
  ---
3558
3500
 
3559
- ## 2. UI & Mockup Animation Rules (Rebuilding Reality)
3501
+ ## Vignette & Noise
3560
3502
 
3561
- UI doesn't just fade in. It pops up with weight.
3562
-
3563
- ### Priority: Use Real Project Components
3564
-
3565
- **If you're in a project folder, ALWAYS check for actual components first:**
3503
+ \`\`\`tsx
3504
+ // Noise
3505
+ <AbsoluteFill style={{
3506
+ backgroundImage: 'url(/noise.png)',
3507
+ opacity: 0.03,
3508
+ mixBlendMode: 'overlay',
3509
+ }} />
3566
3510
 
3567
- \`\`\`bash
3568
- # Before building ANY UI, explore the project
3569
- ls src/components/
3570
- ls src/app/
3511
+ // Vignette
3512
+ <AbsoluteFill style={{
3513
+ background: 'radial-gradient(ellipse at center, transparent 40%, rgba(0,0,0,0.8) 100%)',
3514
+ }} />
3571
3515
  \`\`\`
3516
+ `
3517
+ },
3518
+ {
3519
+ filename: "advanced-techniques.md",
3520
+ content: `# Advanced Techniques
3572
3521
 
3573
- **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
3522
+ ## Audio-Reactive
3579
3523
 
3580
- See [project-based.md](project-based.md) for the full component extraction process.
3524
+ - **Kick:** \`scale(1.005)\` pulse
3525
+ - **Snare:** Trigger scene changes
3526
+ - **Hi-hats:** Cursor flicker, particle shimmer
3581
3527
 
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)
3528
+ ---
3586
3529
 
3587
- ### The "Pop-Up" Entrance (2.5D Rotation)
3530
+ ## Motion Blur (Fake)
3588
3531
 
3589
3532
  \`\`\`tsx
3590
- // Heavy spring for weighty feel
3591
- const progress = spring({
3592
- frame,
3593
- fps,
3594
- config: { mass: 2, damping: 20, stiffness: 100 },
3595
- });
3596
-
3597
- // Start tilted back, spring to flat
3598
- const rotateX = interpolate(progress, [0, 1], [20, 0]);
3599
- const y = interpolate(progress, [0, 1], [100, 0]);
3600
- const scale = interpolate(progress, [0, 1], [0.8, 1.0]);
3533
+ // Trail: Render 3-4 copies with opacity 0.3, 1-frame delay
3601
3534
 
3602
- <div style={{
3603
- transform: \`perspective(1000px) rotateX(\${rotateX}deg) translateY(\${y}px) scale(\${scale})\`,
3604
- }}>
3605
- {uiComponent}
3606
- </div>
3535
+ // Or drop shadow for fast movement:
3536
+ filter: \`drop-shadow(\${velocityX * 0.5}px \${velocityY * 0.5}px 10px rgba(0,0,0,0.3))\`
3607
3537
  \`\`\`
3608
3538
 
3609
- ### Cursor Simulation
3539
+ ---
3610
3540
 
3611
- **Movement:** Never linear. Cursors move in **Bezier Curves** with an arc.
3541
+ ## 3D Perspective
3612
3542
 
3613
3543
  \`\`\`tsx
3614
- // 1. Spring progress from start to end
3615
- const progress = spring({
3616
- frame: frame - startFrame,
3617
- fps,
3618
- config: { damping: 20, stiffness: 80 }, // Slower, deliberate
3619
- });
3620
-
3621
- // 2. Linear interpolation for X and Y
3622
- const linearX = interpolate(progress, [0, 1], [start.x, end.x]);
3623
- const linearY = interpolate(progress, [0, 1], [start.y, end.y]);
3624
-
3625
- // 3. THE ARC: Parabola that peaks mid-travel (this is the secret sauce)
3626
- const arcHeight = 100; // How much the cursor "loops"
3627
- const arcOffset = Math.sin(progress * Math.PI) * arcHeight;
3628
-
3629
- // 4. Apply arc to Y position
3630
- const cursorY = linearY - arcOffset;
3631
- \`\`\`
3632
-
3633
- **Click Interaction:**
3634
- \`\`\`tsx
3635
- // On click:
3636
- // 1. Cursor scales down
3637
- const cursorScale = isClicking ? 0.8 : 1;
3638
-
3639
- // 2. Button squishes
3640
- const buttonScaleX = isClicking ? 1.05 : 1;
3641
- const buttonScaleY = isClicking ? 0.95 : 1;
3642
-
3643
- // 3. Release both with spring
3644
- \`\`\`
3645
-
3646
- **Standard Cursor SVG:**
3647
- \`\`\`tsx
3648
- // Mac-style cursor (use as reference, customize as needed)
3649
- <svg width="32" height="32" viewBox="0 0 32 32" fill="none">
3650
- <path
3651
- d="M4 4L11.5 26L16 17.5L25 25L27 23L18.5 15L28 11.5L4 4Z"
3652
- fill="black"
3653
- stroke="white"
3654
- strokeWidth="2"
3655
- />
3656
- </svg>
3657
- \`\`\`
3658
-
3659
- ### Staggered Lists & Grids
3660
-
3661
- **Rule:** NEVER show a list or grid all at once.
3662
-
3663
- \`\`\`tsx
3664
- const STAGGER_FRAMES = 4; // ~0.05s at 60fps
3665
-
3666
- {items.map((item, i) => {
3667
- const delay = i * STAGGER_FRAMES;
3668
- const progress = spring({
3669
- frame: frame - delay,
3670
- fps,
3671
- config: { damping: 15, stiffness: 120 },
3672
- });
3673
-
3674
- return (
3675
- <div style={{
3676
- opacity: progress,
3677
- transform: \`translateY(\${interpolate(progress, [0, 1], [20, 0])}px)\`,
3678
- }}>
3679
- {item}
3680
- </div>
3681
- );
3682
- })}
3544
+ <div style={{ perspective: '1000px' }}>
3545
+ <div style={{
3546
+ transform: 'rotateX(5deg) rotateY(10deg)',
3547
+ transformStyle: 'preserve-3d',
3548
+ }}>
3549
+ {/* Your UI */}
3550
+ </div>
3551
+ </div>
3683
3552
  \`\`\`
3684
3553
 
3685
3554
  ---
3686
3555
 
3687
- ## 3. Kinetic Typography (Text that Hits)
3688
-
3689
- Text doesn't fade. It slams in, changes fill, or slides up.
3690
-
3691
- ### Pattern A: The "Masked Reveal"
3692
-
3693
- Text rises from a floor:
3556
+ ## Kinetic Typography
3694
3557
 
3558
+ ### Masked Reveal
3695
3559
  \`\`\`tsx
3696
3560
  <div style={{ overflow: 'hidden', height: 80 }}>
3697
3561
  <h1 style={{
@@ -3702,886 +3566,766 @@ Text rises from a floor:
3702
3566
  </div>
3703
3567
  \`\`\`
3704
3568
 
3705
- ### Pattern B: Variable Weight (Animate Keywords)
3569
+ ### Keyword Animation
3570
+ Animate keywords, not whole sentences.
3571
+ `
3572
+ },
3573
+ {
3574
+ filename: "remotion-config.md",
3575
+ content: `# Remotion Configuration
3706
3576
 
3707
- Don't animate the whole sentence. Animate **keywords**:
3577
+ ## FPS & Resolution
3708
3578
 
3709
- \`\`\`tsx
3710
- // "Teams waste HOURS" - only "HOURS" animates
3711
- <p>
3712
- Teams waste{' '}
3713
- <span style={{
3714
- transform: \`scale(\${interpolate(keywordProgress, [0, 1], [1, 1.2])})\`,
3715
- color: interpolateColors(keywordProgress, [0, 1], ['#F0F0F0', '#FF6B6B']),
3716
- textShadow: \`0 0 \${glowProgress * 20}px rgba(255,107,107,0.5)\`,
3717
- }}>
3718
- HOURS
3719
- </span>
3720
- </p>
3721
- \`\`\`
3579
+ - **60 FPS mandatory** \u2014 30fps looks choppy
3580
+ - **1920\xD71080** Full HD
3581
+ - **Center:** \`{x: 960, y: 540}\`
3722
3582
 
3723
- ### Pattern C: The "Glitch/Tech" Accent
3583
+ ---
3724
3584
 
3725
- Chromatic aberration on impact:
3585
+ ## Timing
3726
3586
 
3727
- \`\`\`tsx
3728
- // Duplicate text, offset by 2px with color channels, flash for 2 frames
3729
- {isImpactFrame && (
3730
- <>
3731
- <span style={{ position: 'absolute', left: -2, color: '#FF0000', opacity: 0.5 }}>TEXT</span>
3732
- <span style={{ position: 'absolute', left: 2, color: '#0000FF', opacity: 0.5 }}>TEXT</span>
3733
- </>
3734
- )}
3735
- \`\`\`
3587
+ - 1 second = 60 frames
3588
+ - Fast interaction = 15-20 frames
3589
+ - No scene > 8 seconds without action
3590
+
3591
+ ---
3736
3592
 
3737
- ### Text Colors
3593
+ ## Entry-Action-Exit Structure
3738
3594
 
3739
- Never pure white (\`#FFF\`). Use \`#F0F0F0\` with subtle gradient or shadow for depth.
3595
+ | Phase | Duration |
3596
+ |-------|----------|
3597
+ | Entry | 0.0s - 0.5s |
3598
+ | Action | 0.5s - (duration - 1s) |
3599
+ | Exit | last 1s |
3740
3600
 
3741
3601
  ---
3742
3602
 
3743
- ## 4. Atmosphere & Backgrounds (The "Deep Space")
3603
+ ## Font Loading
3744
3604
 
3745
- Background is never a solid color. It's deep black with floating colored orbs.
3605
+ \`\`\`tsx
3606
+ const [handle] = useState(() => delayRender());
3746
3607
 
3747
- ### The "Orb" System
3608
+ useEffect(() => {
3609
+ document.fonts.ready.then(() => {
3610
+ continueRender(handle);
3611
+ });
3612
+ }, [handle]);
3613
+ \`\`\`
3748
3614
 
3749
- \`\`\`tsx
3750
- const MovingBackground: React.FC = () => {
3751
- const frame = useCurrentFrame();
3615
+ ---
3752
3616
 
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;
3617
+ ## Zod Schema
3758
3618
 
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
- };
3619
+ \`\`\`tsx
3620
+ export const SceneSchema = z.object({
3621
+ titleText: z.string(),
3622
+ buttonColor: z.string(),
3623
+ cursorPath: z.array(z.object({ x: z.number(), y: z.number() })),
3624
+ });
3789
3625
  \`\`\`
3790
3626
 
3791
- ### Vignette & Noise
3627
+ ---
3792
3628
 
3793
- \`\`\`tsx
3794
- // Noise texture overlay (5% opacity)
3795
- <AbsoluteFill style={{
3796
- backgroundImage: 'url(/noise.png)',
3797
- opacity: 0.05,
3798
- mixBlendMode: 'overlay',
3799
- }} />
3629
+ ## SaaS Video Kit Components
3800
3630
 
3801
- // Vignette (dark corners)
3802
- <AbsoluteFill style={{
3803
- background: 'radial-gradient(ellipse at center, transparent 40%, rgba(0,0,0,0.8) 100%)',
3804
- }} />
3805
- \`\`\`
3631
+ | Component | Purpose |
3632
+ |-----------|---------|
3633
+ | \`MockWindow\` | macOS window with traffic lights |
3634
+ | \`SmartCursor\` | Bezier curves + click physics |
3635
+ | \`NotificationToast\` | Slide in, wait, slide out |
3636
+ | \`TypingText\` | Typewriter with cursor |
3637
+ | \`Placeholder\` | For logos/icons |
3806
3638
 
3807
3639
  ---
3808
3640
 
3809
- ## 5. Physics & Timing Reference
3641
+ ## Code Rules
3810
3642
 
3811
- ### Spring Configs
3643
+ 1. No \`transition: all 0.3s\` \u2014 use \`interpolate()\` or \`spring()\`
3644
+ 2. Use \`AbsoluteFill\` for layout
3645
+ 3. No magic numbers \u2014 extract to constants
3646
+ `
3647
+ },
3648
+ {
3649
+ filename: "elite-production.md",
3650
+ content: `# Elite Production
3812
3651
 
3813
- | 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 |
3652
+ For Stripe/Apple/Linear quality.
3818
3653
 
3819
- ### Timing Values
3654
+ ---
3820
3655
 
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 |
3656
+ ## Global Lighting Engine
3827
3657
 
3828
- ### The Golden Rule
3658
+ \`\`\`tsx
3659
+ const lightSource = { x: 0.2, y: -0.5 };
3660
+ const gradientAngle = Math.atan2(lightSource.y, lightSource.x) * (180 / Math.PI);
3829
3661
 
3830
- Use \`interpolate(frame)\` to ensure \`scale\` or \`rotation\` is changing on **EVERY frame**. No static resting states.
3662
+ <button style={{
3663
+ background: \`linear-gradient(\${gradientAngle}deg, rgba(255,255,255,0.1) 0%, transparent 50%)\`,
3664
+ borderTop: '1px solid rgba(255,255,255,0.15)',
3665
+ boxShadow: \`\${-lightSource.x * 20}px \${-lightSource.y * 20}px 40px rgba(0,0,0,0.3)\`,
3666
+ }} />
3667
+ \`\`\`
3831
3668
 
3832
3669
  ---
3833
3670
 
3834
- ## 6. Micro-Tricks from Premium Videos
3835
-
3836
- ### The "Match Cut"
3671
+ ## Noise & Dithering
3837
3672
 
3838
- Small circle zooms to fill screen, becomes next scene's background:
3673
+ Every background needs noise overlay (opacity 0.02-0.05). Prevents YouTube banding.
3839
3674
 
3840
- \`\`\`tsx
3841
- // Circle element scales 50x to wipe to next scene
3842
- const circleScale = interpolate(frame, [MATCH_START, MATCH_END], [1, 50]);
3843
- \`\`\`
3675
+ ---
3844
3676
 
3845
- ### Search Bar Typing
3677
+ ## React Three Fiber
3846
3678
 
3847
- Include blinking cursor:
3679
+ For particles, 3D globes \u2014 use WebGL via \`@remotion/three\`, not CSS 3D.
3848
3680
 
3849
3681
  \`\`\`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
3682
+ import { ThreeCanvas } from '@remotion/three';
3854
3683
 
3855
- return (
3856
- <span>
3857
- {text.slice(0, charIndex)}
3858
- {showCursor && <span style={{ opacity: 0.8 }}>|</span>}
3859
- </span>
3860
- );
3861
- };
3684
+ <AbsoluteFill>
3685
+ <HtmlUI />
3686
+ <ThreeCanvas>
3687
+ <Particles />
3688
+ </ThreeCanvas>
3689
+ </AbsoluteFill>
3862
3690
  \`\`\`
3863
3691
 
3864
- ### The "Card Fan"
3692
+ See \`threejs-*\` skills for implementation.
3865
3693
 
3866
- Stack cards, then fan out:
3694
+ ---
3867
3695
 
3868
- \`\`\`tsx
3869
- const cards = [
3870
- { rotation: -5, x: -20 },
3871
- { rotation: 0, x: 0 },
3872
- { rotation: 5, x: 20 },
3873
- ];
3696
+ ## Virtual Camera Rig
3697
+
3698
+ Move camera, not elements:
3874
3699
 
3875
- {cards.map((card, i) => {
3876
- const fanProgress = spring({ frame: frame - 30, fps, config: { damping: 15, stiffness: 100 } });
3700
+ \`\`\`tsx
3701
+ const CameraProvider = ({ children }) => {
3702
+ const frame = useCurrentFrame();
3703
+ const panX = interpolate(frame, [0, 300], [0, -100]);
3704
+ const zoom = interpolate(frame, [0, 300], [1, 1.05]);
3877
3705
 
3878
3706
  return (
3879
3707
  <div style={{
3880
- position: 'absolute',
3881
- transform: \`rotate(\${fanProgress * card.rotation}deg) translateX(\${fanProgress * card.x}px)\`,
3882
- zIndex: i,
3708
+ transform: \`translateX(\${panX}px) scale(\${zoom})\`,
3709
+ transformOrigin: 'center',
3883
3710
  }}>
3884
- <ProfileCard />
3711
+ {children}
3885
3712
  </div>
3886
3713
  );
3887
- })}
3714
+ };
3888
3715
  \`\`\`
3889
3716
 
3890
3717
  ---
3891
3718
 
3892
- ## 7. Strict Animation Guidelines (DO NOT DEVIATE)
3719
+ ## Motion Rules
3893
3720
 
3894
- **ROLE:** Senior Motion Graphics Engineer for Remotion.
3895
- **REFERENCE STYLE:** "High-End SaaS Product Launch" (e.g., Affable.ai, Linear.app).
3721
+ - **Overshoot:** Modal scales to 1.02, settles to 1.0
3722
+ - **Overlap:** Scene B starts 15 frames before Scene A ends
3723
+ `
3724
+ },
3725
+ {
3726
+ filename: "known-issues.md",
3727
+ content: `# Known Issues & Fixes
3896
3728
 
3897
- ### Physics & Timing
3729
+ ## 1. Music Ends Before Video Finishes
3898
3730
 
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.
3731
+ **Problem:** Music duration is shorter than video duration, causing awkward silence at the end.
3903
3732
 
3904
- ### UI Component Behavior
3733
+ **Solution:** Loop music in Remotion using the \`loop\` prop:
3905
3734
 
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
3735
+ \`\`\`tsx
3736
+ import { Audio } from 'remotion';
3914
3737
 
3915
- ### Typography Patterns
3738
+ <Audio src={musicSrc} volume={0.3} loop />
3739
+ \`\`\`
3916
3740
 
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.
3741
+ **How it works:**
3742
+ - Music automatically loops to fill video duration
3743
+ - Set volume to 0.3 (30% - less loud than voice)
3744
+ - Add fade out at the end for smooth ending
3920
3745
 
3921
- ### Background "Aliveness"
3746
+ ---
3922
3747
 
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.
3748
+ ## 2. Music Transitions Sound Abrupt
3926
3749
 
3927
- ---
3750
+ **Problem:** Music cuts harshly when scenes change or video ends.
3928
3751
 
3929
- ## 8. Seamless Scene Transitions (No Hard Cuts)
3752
+ **Fix in Remotion:**
3753
+ \`\`\`tsx
3754
+ import { interpolate, Audio } from 'remotion';
3930
3755
 
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.
3756
+ // Fade music in/out at scene boundaries
3757
+ const musicVolume = interpolate(
3758
+ frame,
3759
+ [0, 30, totalFrames - 60, totalFrames],
3760
+ [0, 0.3, 0.3, 0],
3761
+ { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }
3762
+ );
3932
3763
 
3933
- ### The "Wipe" Rule (No Hard Cuts)
3764
+ <Audio src={music} volume={musicVolume} />
3765
+ \`\`\`
3934
3766
 
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).
3767
+ ---
3938
3768
 
3939
- ### The "Zoom-Through" Technique (Best for SaaS)
3769
+ ## 3. Scene Transitions Too Harsh
3940
3770
 
3941
- As Scene A ends, the camera must **accelerate** its zoom:
3771
+ **Problem:** Scenes change abruptly without smooth transitions.
3942
3772
 
3773
+ **Fix:** Use \`@remotion/transitions\` with overlapping:
3943
3774
  \`\`\`tsx
3944
- // Scene A Action: At duration - 20 frames, zoom way in
3945
- const scaleA = interpolate(frame, [duration - 20, duration], [1.0, 5.0]);
3946
-
3947
- // Scene B Action: Starts at scale 0.5 and springs to 1.0
3948
- const scaleB = spring({ frame, fps, config: { damping: 14, stiffness: 120 } });
3949
- const scaleBValue = interpolate(scaleB, [0, 1], [0.5, 1]);
3775
+ import { TransitionSeries, springTiming } from '@remotion/transitions';
3776
+ import { slide } from '@remotion/transitions/slide';
3777
+ import { fade } from '@remotion/transitions/fade';
3778
+
3779
+ <TransitionSeries>
3780
+ <TransitionSeries.Sequence durationInFrames={sceneA.frames}>
3781
+ <SceneA />
3782
+ </TransitionSeries.Sequence>
3783
+ <TransitionSeries.Transition
3784
+ presentation={slide({ direction: 'from-right' })}
3785
+ timing={springTiming({ config: { damping: 20 } })}
3786
+ />
3787
+ <TransitionSeries.Sequence durationInFrames={sceneB.frames}>
3788
+ <SceneB />
3789
+ </TransitionSeries.Sequence>
3790
+ </TransitionSeries>
3950
3791
  \`\`\`
3951
3792
 
3952
- Result: It looks like the camera flew *through* Scene A to find Scene B behind it.
3793
+ ---
3953
3794
 
3954
- ### The "Color Swipe" (Safety Net)
3795
+ ## 4. Voiceover Lacks Energy
3955
3796
 
3956
- If a complex match-cut is too hard, use a "Curtain":
3797
+ **Problem:** Voiceover sounds flat/monotone.
3957
3798
 
3958
- \`\`\`tsx
3959
- // A solid colored AbsoluteFill slides in from left, covers screen for 2 frames,
3960
- // then slides out to the right, revealing Scene B
3961
- const curtainX = interpolate(frame, [0, 10, 12, 22], [-100, 0, 0, 100]);
3799
+ **Fix:** Pass \`voiceSettings\` in scenes JSON:
3800
+ \`\`\`json
3801
+ {
3802
+ "scenes": [...],
3803
+ "voice": "Kore",
3804
+ "voiceSettings": {
3805
+ "style": 0.6,
3806
+ "stability": 0.4,
3807
+ "speed": 0.95
3808
+ }
3809
+ }
3962
3810
  \`\`\`
3963
3811
 
3964
- ### TransitionWrapper Component
3812
+ - \`style\`: 0.5-0.7 for more expressive delivery
3813
+ - \`stability\`: 0.3-0.5 for more variation
3814
+ - \`speed\`: 0.9-1.0 slightly slower = more impactful
3965
3815
 
3966
- Use the \`TransitionWrapper\` from shared components:
3816
+ ---
3967
3817
 
3968
- \`\`\`tsx
3969
- import { TransitionWrapper } from './shared';
3818
+ ## 5. Video Duration Mismatch
3970
3819
 
3971
- // Types: 'slide' | 'zoom' | 'fade' | 'none'
3972
- // enterFrom: 'left' | 'right' | 'bottom'
3820
+ **Problem:** Brief says 30-45s but video is 20s (because scene duration = voiceover duration).
3973
3821
 
3974
- <TransitionWrapper type="slide" enterFrom="bottom">
3975
- <YourScene />
3976
- </TransitionWrapper>
3822
+ **Fixes:**
3823
+ 1. **Slow voice:** Use \`speed: 0.85\` in voiceSettings
3824
+ 2. **Add padding in Remotion:** Hold last frame, add breathing room
3825
+ \`\`\`tsx
3826
+ // Add 30 frames (0.5s) padding after voiceover ends
3827
+ const paddedDuration = voiceoverFrames + 30;
3977
3828
  \`\`\`
3829
+ 3. **Brief should note:** "Duration based on voiceover length"
3830
+
3831
+ ---
3832
+
3833
+ ## 6. Not Using Project UI Components
3978
3834
 
3979
- ### Critical: Overlap Your Sequences
3835
+ **Problem:** Generic UI instead of pixel-perfect project components.
3980
3836
 
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).
3837
+ **Fix:** In Phase 1 Discovery:
3838
+ 1. Find project's actual components (buttons, cards, modals, inputs)
3839
+ 2. Copy their styles/structure into Remotion components
3840
+ 3. Match colors, fonts, shadows, border-radius exactly
3982
3841
 
3983
3842
  \`\`\`tsx
3984
- 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
- };
3843
+ // DON'T: Generic button
3844
+ <button style={{ background: 'blue' }}>Click</button>
3845
+
3846
+ // DO: Match project's actual button
3847
+ <button style={{
3848
+ background: 'linear-gradient(135deg, #6366f1, #8b5cf6)',
3849
+ borderRadius: 8,
3850
+ padding: '12px 24px',
3851
+ boxShadow: '0 4px 14px rgba(99, 102, 241, 0.4)',
3852
+ border: '1px solid rgba(255,255,255,0.1)',
3853
+ }}>Click</button>
4009
3854
  \`\`\`
4010
3855
 
4011
- **Why this fixes the "Light Switch" effect:** Because Scene 2 physically slides *over* Scene 1 while Scene 1 is still visible underneath, your brain registers it as continuous movement rather than a "cut."
4012
-
4013
3856
  ---
4014
3857
 
4015
- ## Quick Tips from Motion Devs
3858
+ ## 7. Missing Physics & Lighting
4016
3859
 
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.
3860
+ **Problem:** Video feels flat, no depth or motion.
3861
+
3862
+ **Checklist:**
3863
+ - [ ] Global light source defined (affects all shadows/gradients)
3864
+ - [ ] Camera rig with subtle drift/zoom
3865
+ - [ ] Spring physics on ALL entrances (no linear)
3866
+ - [ ] Staggered animations (never all at once)
3867
+ - [ ] Background orbs/particles moving
3868
+ - [ ] Noise overlay (opacity 0.02-0.05)
3869
+ - [ ] Vignette for depth
3870
+ `
3871
+ }
3872
+ ];
4020
3873
 
3874
+ // src/commands/skill/generate-video-skill.ts
3875
+ function generateVideoSkillContent(context) {
3876
+ const { name, cmd: cmd2, displayName } = context;
3877
+ return `---
3878
+ name: ${name}-video
3879
+ description: Use when user asks to create videos (product demos, explainers, social content, promos). Handles video asset generation, Remotion implementation, and thumbnail embedding.
4021
3880
  ---
4022
3881
 
4023
- ## Self-Check: Is It Kinetic?
3882
+ # ${displayName} Video Creation CLI
4024
3883
 
4025
- Before rendering, verify:
3884
+ Create professional product videos directly from your terminal. The CLI generates AI-powered video assets (voiceover, music, images, stock videos) and provides tools for Remotion-based video production with React Three Fiber.
4026
3885
 
4027
- - [ ] 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
3886
+ **Stack:** Remotion (React video framework) + React Three Fiber (R3F) + Three.js for 3D/WebGL, particles, shaders, lighting.
4037
3887
 
4038
- If your video looks like PowerPoint slides with voiceover, **START OVER**.
4039
- `
4040
- },
4041
- {
4042
- filename: "failures.md",
4043
- content: `# Common Failures - READ THIS FIRST
4044
-
4045
- If your video looks like any of these, START OVER.
3888
+ We create **elite product videos** (Stripe, Apple, Linear quality) using physics-based animation, dynamic lighting, and pixel-perfect UI components rebuilt from the real project \u2014 never boring screenshots or static images.
4046
3889
 
4047
- ## FAILURE 1: Slideshow
4048
-
4049
- \`\`\`
4050
- What you made:
4051
- - Background image
4052
- - Text appears on top
4053
- - Text disappears
4054
- - New background image
4055
- - More text
4056
-
4057
- This is PowerPoint, not a video. REJECTED.
4058
- \`\`\`
3890
+ **Core Philosophy:** "Nothing sits still. Everything is physics-based. Every pixel breathes."
4059
3891
 
4060
- ## FAILURE 2: Lorem Ipsum / Placeholder Content
3892
+ ---
4061
3893
 
4062
- \`\`\`
4063
- What you made:
4064
- - "Lorem ipsum dolor sit amet..."
4065
- - "Sample text here"
4066
- - Generic placeholder content
3894
+ ## CRITICAL: Professional Composition Rules
4067
3895
 
4068
- Use REAL content from the project. REJECTED.
4069
- \`\`\`
3896
+ **These rules are MANDATORY for all marketing/product videos:**
4070
3897
 
4071
- ## FAILURE 3: Static UI Screenshot
3898
+ ### \u274C NEVER DO:
3899
+ 1. **Walls of text** - No dense paragraphs or lists longer than 3 lines
3900
+ 2. **Flying/floating cards** - No random floating animations across the screen
3901
+ 3. **Stretched layouts** - No elements awkwardly stretched to fill space
3902
+ 4. **Truncated text** - Never show "Text that gets cut off..."
3903
+ 5. **Information overload** - Max 1-2 key points visible at once
3904
+ 6. **Amateur motion** - No PowerPoint-style "fly in from left/right"
4072
3905
 
4073
- \`\`\`
4074
- What you made:
4075
- - Screenshot of the app
4076
- - Text overlay saying "Our Dashboard"
4077
- - No motion, no interaction
3906
+ ### \u2705 ALWAYS DO:
3907
+ 1. **Hierarchy first** - One clear focal point per scene (headline OR stat OR visual, not all)
3908
+ 2. **Breathing room** - Generous whitespace (min 100px padding around elements)
3909
+ 3. **Purposeful motion** - Cards appear with subtle spring (0-20px translateY), not fly across screen
3910
+ 4. **Readable text** - Max 2-3 lines per card, 24px+ font size
3911
+ 5. **Grid alignment** - Use invisible grid (3-column or 4-column layout)
3912
+ 6. **Professional entrance** - Elements fade + slight translate (15px max), hold for 2-3s, then exit
4078
3913
 
4079
- Recreate the UI in code and ANIMATE it. REJECTED.
4080
- \`\`\`
4081
-
4082
- ## FAILURE 4: Elements Just Appearing
3914
+ ### Composition Examples:
4083
3915
 
3916
+ **\u274C BAD - Wall of Text:**
3917
+ \`\`\`tsx
3918
+ // DON'T: 10 bullet points crammed in a card
3919
+ <Card>
3920
+ <ul>
3921
+ {[...10items].map(item => <li>{item.longText}...</li>)}
3922
+ </ul>
3923
+ </Card>
4084
3924
  \`\`\`
4085
- What you made:
4086
- - Frame 0: nothing
4087
- - Frame 1: element is fully visible
4088
- - No transition, no animation
4089
3925
 
4090
- Every element must animate in with spring/easing. REJECTED.
3926
+ **\u2705 GOOD - Single Focus:**
3927
+ \`\`\`tsx
3928
+ // DO: One headline, one supporting stat
3929
+ <AbsoluteFill style={{ alignItems: 'center', justifyContent: 'center' }}>
3930
+ <h1 style={{ fontSize: 72, marginBottom: 40 }}>12 hours wasted</h1>
3931
+ <p style={{ fontSize: 28, opacity: 0.7 }}>per week on manual tasks</p>
3932
+ </AbsoluteFill>
4091
3933
  \`\`\`
4092
3934
 
4093
- ## FAILURE 5: Hard Cuts Between Scenes (The "Light Switch" Effect)
4094
-
4095
- \`\`\`
4096
- What you made:
4097
- - Scene 1 ends at frame 100
4098
- - Scene 2 starts at frame 101
4099
- - No overlap, no slide, no zoom-through
4100
- - Feels like a light switching on/off
4101
-
4102
- FIX: Overlap sequences by 15-20 frames. Scene 2 must slide/zoom
4103
- ON TOP of Scene 1 while Scene 1 is still visible. Use TransitionWrapper.
4104
- REJECTED.
3935
+ **\u274C BAD - Flying Cards:**
3936
+ \`\`\`tsx
3937
+ // DON'T: Cards flying from random positions
3938
+ <Card style={{
3939
+ transform: \`translateX(\${interpolate(progress, [0,1], [-500, 0])}px)\` // Flies from left
3940
+ }} />
4105
3941
  \`\`\`
4106
3942
 
4107
- ## FAILURE 6: Static Camera
4108
-
3943
+ **\u2705 GOOD - Subtle Entrance:**
3944
+ \`\`\`tsx
3945
+ // DO: Gentle spring entrance with minimal movement
3946
+ const progress = spring({ frame: frame - startFrame, fps, config: { damping: 20, stiffness: 100 }});
3947
+ <Card style={{
3948
+ opacity: progress,
3949
+ transform: \`translateY(\${interpolate(progress, [0,1], [15, 0])}px)\` // Subtle 15px drop
3950
+ }} />
4109
3951
  \`\`\`
4110
- What you made:
4111
- - Camera never moves
4112
- - No drift, no zoom, no rotation
4113
- - Feels like watching a screenshot
4114
3952
 
4115
- Camera must ALWAYS be subtly moving. REJECTED.
4116
- \`\`\`
3953
+ ### Layout Grid System:
4117
3954
 
4118
- ## FAILURE 7: Linear Cursor Movement
3955
+ **Use 12-column grid (like Bootstrap):**
3956
+ \`\`\`tsx
3957
+ const GRID = {
3958
+ columns: 12,
3959
+ gutter: 40,
3960
+ padding: 120 // Edge padding
3961
+ };
4119
3962
 
3963
+ // Center 6 columns for main content
3964
+ const contentWidth = (1920 - (GRID.padding * 2) - (GRID.gutter * 5)) / 2;
4120
3965
  \`\`\`
4121
- What you made:
4122
- - Cursor moves in straight lines
4123
- - No overshoot on stops
4124
- - Click has no feedback
4125
3966
 
4126
- Cursor must move in Bezier curves with overshoot. REJECTED.
4127
- \`\`\`
4128
- `
4129
- },
4130
- {
4131
- filename: "parameterization.md",
4132
- content: `# Parameterization (Critical)
3967
+ **Positioning anchors:**
3968
+ - **Top-left:** Brand logo, context (10% from edges)
3969
+ - **Center:** Primary headline/stat/demo (50% transform)
3970
+ - **Bottom:** CTA or tagline (10% from bottom)
3971
+ - **Never:** Random floating between these zones
4133
3972
 
4134
- Never hardcode frame numbers. Use variables for all keyframes.
3973
+ ---
4135
3974
 
4136
- ## Why
3975
+ ## Prerequisites
4137
3976
 
4138
- When you change timing early in video, everything else breaks if hardcoded.
3977
+ Before using this skill, ensure you have:
4139
3978
 
4140
- ## Pattern
3979
+ 1. **Load related skills:**
3980
+ \`\`\`
3981
+ remotion-best-practices
3982
+ threejs-fundamentals
3983
+ \`\`\`
4141
3984
 
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;
3985
+ 2. **Authenticate:**
3986
+ \`\`\`bash
3987
+ ${cmd2} login
3988
+ \`\`\`
4150
3989
 
4151
- // Use in animations
4152
- opacity: interpolate(frame, [TITLE_IN, TITLE_IN + 15], [0, 1])
4153
- \`\`\`
3990
+ 3. **Remotion installed** (if creating videos):
3991
+ \`\`\`bash
3992
+ pnpm install remotion @remotion/cli
3993
+ \`\`\`
4154
3994
 
4155
- ## Scene Duration
3995
+ ---
4156
3996
 
4157
- \`\`\`ts
4158
- // Calculate from keyframes, don't hardcode
4159
- const sceneDuration = SCENE_END - SCENE_START;
4160
- \`\`\`
3997
+ ## Video Creation Workflow
4161
3998
 
4162
- ## Audio Sync
3999
+ ### Phase 0: Load Skills (MANDATORY)
4163
4000
 
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
4001
+ Before ANY video work, invoke these skills:
4168
4002
  \`\`\`
4169
-
4170
- ## Multi-Scene
4171
-
4172
- \`\`\`ts
4173
- const SCENE_1_START = 0;
4174
- const SCENE_1_END = SCENE_1_START + 90;
4175
- const SCENE_2_START = SCENE_1_END - 15; // 15 frame overlap for transition
4176
- const SCENE_2_END = SCENE_2_START + 120;
4003
+ remotion-best-practices
4004
+ threejs-fundamentals
4177
4005
  \`\`\`
4178
- `
4179
- },
4180
- {
4181
- filename: "layers.md",
4182
- content: `# Layers & Composition
4183
4006
 
4184
- Explicit z-index prevents visual bugs.
4007
+ ### Phase 1: Discovery
4185
4008
 
4186
- ## Z-Index Scale
4009
+ Explore current directory silently:
4010
+ - Understand what the product does (README, docs, code)
4011
+ - Find branding: logo, colors, fonts
4012
+ - Find UI components to copy into Remotion (buttons, cards, modals, etc.) \u2014 rebuild pixel-perfect, no screenshots
4187
4013
 
4188
- | 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) |
4014
+ ### Phase 2: Video Brief
4198
4015
 
4199
- ## Composition Structure
4016
+ Present a brief outline (scenes \u22648s each, duration, assets found) and get user approval before production.
4200
4017
 
4201
- \`\`\`tsx
4202
- <AbsoluteFill>
4203
- {/* Background layer */}
4204
- <MovingBackground />
4018
+ ### Phase 3: Production
4205
4019
 
4206
- {/* Vignette */}
4207
- <Vignette />
4208
-
4209
- {/* Camera rig wraps all content */}
4210
- <CameraRig>
4211
- <Sequence from={0}>
4212
- <Scene1 />
4213
- </Sequence>
4214
- <Sequence from={SCENE_2_START}>
4215
- <Scene2 />
4216
- </Sequence>
4217
- </CameraRig>
4218
-
4219
- {/* Audio */}
4220
- <Audio src={music} volume={0.3} />
4221
- <Audio src={voiceover} />
4222
- </AbsoluteFill>
4223
- \`\`\`
4020
+ 1. **Generate audio assets** - \`${cmd2} video create\` with scenes JSON
4021
+ - IMPORTANT: Music is generated LAST after all voiceover/audio to ensure exact duration match
4022
+ 2. **Scaffold OUTSIDE project** - \`cd .. && ${cmd2} video init my-video\`
4023
+ 3. **Copy assets + UI components** from project into video project
4024
+ 4. **Implement** - follow rules below
4224
4025
 
4225
- ## Overlapping Sequences
4026
+ ### Phase 4: Render & Thumbnail (REQUIRED)
4226
4027
 
4227
- For zoom-through transitions, scenes overlap:
4028
+ \`\`\`bash
4029
+ # 1. Render the video (with voiceover and music already included)
4030
+ pnpm exec remotion render FullVideo
4228
4031
 
4229
- \`\`\`tsx
4230
- <Sequence from={0} durationInFrames={100}>
4231
- <Scene1 />
4232
- </Sequence>
4233
- <Sequence from={80} durationInFrames={100}> {/* 20 frame overlap */}
4234
- <Scene2 />
4235
- </Sequence>
4032
+ # 2. ALWAYS embed thumbnail before delivering
4033
+ ${cmd2} video thumbnail out/FullVideo.mp4 --frame 60
4236
4034
  \`\`\`
4237
- `
4238
- },
4239
- {
4240
- filename: "project-based.md",
4241
- content: `# Component Extraction (Critical for Product Videos)
4242
4035
 
4243
- When creating a video for a project, **copy the actual components** - don't rebuild from scratch.
4036
+ **Note:** Remotion videos include per-scene voiceovers and background music baked in during render.
4244
4037
 
4245
- ## The Goal
4246
-
4247
- Your video should show the REAL product UI, pixel-perfect. Viewers should not be able to tell the difference between a screenshot and your video (except yours is animated with kinetic motion).
4038
+ ---
4248
4039
 
4249
- ## Process: Eject \u2192 Simplify \u2192 Animate
4040
+ ## Asset Generation
4250
4041
 
4251
- ### Step 1: Find Components to Feature
4042
+ Generate voiceover, music, and visual assets for each scene:
4252
4043
 
4253
4044
  \`\`\`bash
4254
- # Explore the project structure
4255
- ls src/components/
4256
- ls src/app/
4257
- # Find the key UI: dashboards, forms, cards, modals, etc.
4045
+ cat <<SCENES | ${cmd2} video create --output ./public
4046
+ {
4047
+ "scenes": [
4048
+ {
4049
+ "name": "Hook",
4050
+ "script": "Watch how we transformed this complex workflow into a single click.",
4051
+ "imageQuery": "modern dashboard interface dark theme",
4052
+ "videoQuery": "abstract tech particles animation"
4053
+ },
4054
+ {
4055
+ "name": "Demo",
4056
+ "script": "Our AI analyzes your data in real-time, surfacing insights that matter.",
4057
+ "imageQuery": "data visualization charts analytics"
4058
+ },
4059
+ {
4060
+ "name": "CTA",
4061
+ "script": "Start your free trial today. No credit card required.",
4062
+ "imageQuery": "call to action button modern"
4063
+ }
4064
+ ],
4065
+ "voice": "Kore",
4066
+ "voiceSettings": {
4067
+ "style": 0.6,
4068
+ "stability": 0.4,
4069
+ "speed": 0.95
4070
+ },
4071
+ "musicPrompt": "upbeat corporate, positive energy, modern synth"
4072
+ }
4073
+ SCENES
4258
4074
  \`\`\`
4259
4075
 
4260
- ### Step 2: Copy Component Code
4076
+ **Output:**
4077
+ - \`public/audio/Hook.mp3\` - scene voiceovers
4078
+ - \`public/audio/music.mp3\` - background music (30s max)
4079
+ - \`public/video-manifest.json\` - timing and metadata
4080
+ - Stock images/videos (if requested)
4261
4081
 
4262
- Copy the actual component file into your Remotion project.
4082
+ ---
4263
4083
 
4264
- ### Step 3: Eject - Strip Logic, Keep Visuals
4084
+ ## Core Video Rules
4265
4085
 
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
- }
4086
+ ${VIDEO_RULE_CONTENTS.map((rule) => rule.content).join("\n\n---\n\n")}
4273
4087
 
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
4088
+ ## Useful Commands
4278
4089
 
4279
- const entranceProgress = spring({ frame: progress, fps, config: { mass: 2, damping: 20, stiffness: 100 } });
4280
- const rotateX = interpolate(entranceProgress, [0, 1], [20, 0]);
4281
-
4282
- return (
4283
- <div style={{
4284
- transform: \`perspective(1000px) rotateX(\${rotateX}deg)\`,
4285
- // ... exact same visual styles as original
4286
- }}>
4287
- ...
4288
- </div>
4289
- );
4290
- }
4291
- \`\`\`
4090
+ \`\`\`bash
4091
+ # Generate video assets
4092
+ ${cmd2} video create < scenes.json
4093
+ cat scenes.json | ${cmd2} video create --output ./public
4292
4094
 
4293
- ### Step 4: Add Kinetic Animation
4095
+ # Initialize Remotion project
4096
+ ${cmd2} video init my-video
4294
4097
 
4295
- Apply the rules from kinetic-saas.md:
4296
- - 2.5D rotation entrance
4297
- - Spring physics
4298
- - Staggered children
4299
- - Cursor interaction states
4098
+ # Embed thumbnail
4099
+ ${cmd2} video thumbnail out/video.mp4 --frame 60
4300
4100
 
4301
- ### Step 5: Fill With Real Demo Data
4101
+ # Search for stock assets
4102
+ ${cmd2} images search "mountain landscape" --limit 10
4103
+ ${cmd2} videos search "ocean waves" --limit 5
4302
4104
 
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"
4105
+ # Generate audio
4106
+ ${cmd2} audio generate "Your script here" --voice Kore
4107
+ ${cmd2} music generate "upbeat corporate" --duration 30
4310
4108
  \`\`\`
4311
4109
 
4312
- ## What to Copy
4110
+ ---
4313
4111
 
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 |
4112
+ ## Best Practices
4320
4113
 
4321
- ## Common Mistakes
4114
+ 1. **Keep scenes under 8 seconds** without cuts or major action
4115
+ 2. **Use spring physics** for all animations, never linear
4116
+ 3. **Rebuild UI components** in React/CSS, no screenshots
4117
+ 4. **Test with thumbnail embedding** before delivering
4118
+ 5. **Music volume at 30%** (30-40% less loud than voice)
4119
+ 6. **Read all video rules** in Phase 0 before implementation
4322
4120
 
4323
- \u274C Using placeholder colors like \`#333\`
4324
- \u2705 Using exact project colors from tailwind config
4121
+ ---
4325
4122
 
4326
- \u274C Generic content: "Feature 1", "Lorem ipsum"
4327
- \u2705 Real content: "Unlimited projects", "Priority support"
4123
+ ## Troubleshooting
4328
4124
 
4329
- \u274C Building from scratch based on screenshots
4330
- \u2705 Copying actual component code and ejecting it
4125
+ If you encounter issues:
4126
+ - Check authentication: \`${cmd2} whoami\`
4127
+ - Verify asset generation: check \`video-manifest.json\`
4128
+ - Voiceover flat: increase style (0.5-0.7), decrease stability (0.3-0.5)
4129
+ - Duration mismatch: adjust \`voiceSettings.speed\` or add padding in Remotion
4331
4130
 
4332
- \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)
4131
+ For detailed troubleshooting, see "Known Issues" section above.
4132
+ `;
4133
+ }
4134
+
4135
+ // src/commands/skill/generate-presentation-skill.ts
4136
+ function generatePresentationSkillContent(context) {
4137
+ const { name, cmd: cmd2, displayName } = context;
4138
+ return `---
4139
+ name: ${name}-presentation
4140
+ description: Use when user asks to create presentations (slides, decks, pitch decks). Generates AI-powered presentations with structured content and professional design.
4141
+ ---
4339
4142
 
4340
- **Platforms:** TikTok, Instagram Reels, YouTube Shorts (all 9:16 vertical)
4143
+ # ${displayName} Presentation CLI
4341
4144
 
4342
- **Philosophy:** "Don't make ads. Make TikToks." Your video must look native to the platform - lo-fi beats polished.
4145
+ Create professional presentations (slides, decks, pitch decks) directly from your terminal. The CLI generates AI-powered slides from any context you provide - text, files, URLs, or piped content. Also supports searching for stock images and videos.
4343
4146
 
4344
4147
  ---
4345
4148
 
4346
- ## 1. The "3-Second War"
4347
-
4348
- You have 3 seconds before the thumb scrolls. Win or lose everything.
4149
+ ## Authentication
4349
4150
 
4350
- ### Hook Requirements (0-3s)
4151
+ \`\`\`bash
4152
+ # Login via OAuth
4153
+ ${cmd2} login
4351
4154
 
4352
- - **Text on screen in FRAME 1** - no waiting for audio
4353
- - **Movement** - never start static
4354
- - **Pattern interrupt** - something unexpected
4155
+ # Or set API key
4156
+ export ${name.toUpperCase().replace(/-/g, "_")}_API_KEY="your-key-here"
4157
+ \`\`\`
4355
4158
 
4356
- ### Hook Types
4159
+ ---
4357
4160
 
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 |
4161
+ ## Creating Presentations
4364
4162
 
4365
- ### Audio Hooks
4163
+ ### From Text
4366
4164
 
4367
- Bad: "Today I want to talk about..."
4368
- Good: "Stop scrolling if you hate [X]." / "This feels illegal to know."
4165
+ \`\`\`bash
4166
+ ${cmd2} create "AI-powered product analytics platform"
4167
+ \`\`\`
4369
4168
 
4370
- ---
4169
+ ### From File
4371
4170
 
4372
- ## 2. Pacing Rules (No Dead Air)
4171
+ \`\`\`bash
4172
+ ${cmd2} create --file product-brief.md
4173
+ \`\`\`
4373
4174
 
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 |
4175
+ ### From URL
4380
4176
 
4381
- ---
4177
+ \`\`\`bash
4178
+ ${cmd2} create --url https://company.com/product
4179
+ \`\`\`
4382
4180
 
4383
- ## 3. Native Lo-Fi Aesthetic
4181
+ ### From Piped Content
4384
4182
 
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 |
4183
+ \`\`\`bash
4184
+ cat research.txt | ${cmd2} create
4185
+ pbpaste | ${cmd2} create
4186
+ \`\`\`
4391
4187
 
4392
- ### Native Font Stack
4188
+ ### Advanced Options
4393
4189
 
4394
- \`\`\`tsx
4395
- const NATIVE_FONT = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif";
4190
+ \`\`\`bash
4191
+ ${cmd2} create "Topic" \\
4192
+ --slides 10 \\
4193
+ --style professional \\
4194
+ --branding my-brand \\
4195
+ --template minimal \\
4196
+ --output presentation.zip
4396
4197
  \`\`\`
4397
4198
 
4398
4199
  ---
4399
4200
 
4400
- ## 4. Safe Zone Rules (CRITICAL)
4201
+ ## Presentation Options
4401
4202
 
4402
- Platform UI overlays your content. Plan for it.
4203
+ - **\`--slides <count>\`** - Number of slides (default: 8-12 based on content)
4204
+ - **\`--style <style>\`** - Presentation style: professional, creative, minimal, corporate
4205
+ - **\`--branding <name>\`** - Use saved branding profile
4206
+ - **\`--template <name>\`** - Design template to use
4207
+ - **\`--output <file>\`** - Export to file (.zip, .pptx, .pdf)
4208
+ - **\`--format <format>\`** - Output format: human, json, quiet
4403
4209
 
4404
- \`\`\`
4405
- 1080x1920 Canvas
4406
- +------------------------------------------+
4407
- | TOP SAFE ZONE (150px) | <- Notch, status bar
4408
- | ----------------------------------------|
4409
- | |
4410
- | CONTENT SAFE ZONE |
4411
- | (60px left, 120px right margins) |
4412
- | |
4413
- | ----------------------------------------|
4414
- | BOTTOM SAFE ZONE (384px / 20%) | <- Captions, username
4415
- | [Icon strip on right: 120px] | <- Like, Comment, Share
4416
- +------------------------------------------+
4417
- \`\`\`
4210
+ ---
4418
4211
 
4419
- ### Safe Zone Values
4212
+ ## Managing Presentations
4420
4213
 
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
- };
4214
+ \`\`\`bash
4215
+ # List all presentations
4216
+ ${cmd2} list
4217
+ ${cmd2} list --format json
4428
4218
 
4429
- // Captions must sit ABOVE bottom safe zone
4430
- const CAPTION_BOTTOM = 350; // px from bottom
4431
- \`\`\`
4219
+ # Get presentation details
4220
+ ${cmd2} get <id-or-slug>
4432
4221
 
4433
- ### SafeZoneGuide Component
4222
+ # Export presentation
4223
+ ${cmd2} export <id-or-slug> -o presentation.zip
4434
4224
 
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
- );
4225
+ # Import presentation
4226
+ ${cmd2} import ./presentation.zip
4227
+
4228
+ # Delete presentation
4229
+ ${cmd2} delete <id-or-slug>
4455
4230
  \`\`\`
4456
4231
 
4457
4232
  ---
4458
4233
 
4459
- ## 5. Content Formats
4234
+ ## Branding Management
4460
4235
 
4461
- ### Green Screen (Speaker + Background)
4462
-
4463
- \`\`\`tsx
4464
- <AbsoluteFill>
4465
- <Img src={backgroundImage} style={{ width: "100%", height: "100%", objectFit: "cover" }} />
4466
- <div style={{
4467
- position: "absolute", bottom: 400, left: 40,
4468
- width: 300, height: 400, borderRadius: 20, overflow: "hidden",
4469
- }}>
4470
- <Video src={speakerVideo} />
4471
- </div>
4472
- </AbsoluteFill>
4473
- \`\`\`
4236
+ \`\`\`bash
4237
+ # List saved brands
4238
+ ${cmd2} branding list
4474
4239
 
4475
- ### Listicle (Rapid-Fire)
4240
+ # Extract branding from website
4241
+ ${cmd2} branding extract https://company.com
4476
4242
 
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
4243
+ # Use branding in presentation
4244
+ ${cmd2} create "Topic" --branding company-brand
4481
4245
  \`\`\`
4482
4246
 
4483
- ### Visual ASMR (No Talking)
4247
+ ---
4484
4248
 
4485
- - No voiceover, music only
4486
- - Slow, deliberate cursor movements
4487
- - Satisfying click feedback
4488
- - Ken Burns zoom on details
4249
+ ## Stock Asset Search
4489
4250
 
4490
- ### POV Skit
4251
+ \`\`\`bash
4252
+ # Search for images
4253
+ ${cmd2} images search "mountain landscape" --limit 10
4254
+ ${cmd2} images search "business team" --format json
4491
4255
 
4492
- \`\`\`tsx
4493
- <div style={{ position: "absolute", top: 160, left: 40, right: 120, fontSize: 42 }}>
4494
- POV: {setup}
4495
- </div>
4256
+ # Search for videos
4257
+ ${cmd2} videos search "ocean waves" --limit 5
4258
+ ${cmd2} videos search "city timelapse" --orientation landscape
4496
4259
  \`\`\`
4497
4260
 
4498
4261
  ---
4499
4262
 
4500
- ## 6. TikTok Captions
4263
+ ## Best Practices
4501
4264
 
4502
- Use \`@remotion/captions\` with \`createTikTokStyleCaptions\`:
4265
+ 1. **Provide context** - More input = better presentations
4266
+ 2. **Use branding** - Extract and apply brand consistency
4267
+ 3. **Review content** - AI-generated content should be reviewed
4268
+ 4. **Export for sharing** - Use \`--output\` to create shareable files
4269
+ 5. **Iterate** - Regenerate specific slides if needed
4503
4270
 
4504
- \`\`\`tsx
4505
- import { createTikTokStyleCaptions } from "@remotion/captions";
4271
+ ---
4506
4272
 
4507
- const { pages } = createTikTokStyleCaptions({
4508
- captions: transcriptCaptions,
4509
- combineTokensWithinMilliseconds: 1200, // Higher = more words per page
4510
- });
4511
- \`\`\`
4273
+ ## Troubleshooting
4512
4274
 
4513
- ### Word-by-Word Highlighting
4275
+ **Authentication Issues:**
4276
+ \`\`\`bash
4277
+ # Check current user
4278
+ ${cmd2} whoami
4514
4279
 
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
- })}
4280
+ # Re-authenticate
4281
+ ${cmd2} logout
4282
+ ${cmd2} login
4529
4283
  \`\`\`
4530
4284
 
4531
- ---
4532
-
4533
- ## 7. Script Structure (60 Seconds)
4534
-
4535
- | Timestamp | Section | Purpose |
4536
- |-----------|---------|---------|
4537
- | 0:00-0:03 | **HOOK** | Pattern interrupt, bold claim |
4538
- | 0:03-0:10 | **PROBLEM** | Agitate the pain point |
4539
- | 0:10-0:40 | **SOLUTION** | Rapid value delivery |
4540
- | 0:40-0:55 | **PROOF** | Show, don't tell |
4541
- | 0:55-0:60 | **CTA** | Soft ask (Follow/Link in bio) |
4285
+ **Generation Failures:**
4286
+ - Ensure input is clear and has enough context
4287
+ - Try different styles or templates
4288
+ - Check API status and quotas
4542
4289
 
4543
- ### CTA Best Practices
4544
-
4545
- | Do | Don't |
4546
- |----|-------|
4547
- | "Follow for more tips" | "SMASH that like button" |
4548
- | "Link in bio" | "Click the link below" |
4549
- | "Save this for later" | "Subscribe to my channel" |
4290
+ **Export Issues:**
4291
+ - Verify output format is supported
4292
+ - Check file permissions in output directory
4293
+ - Ensure presentation ID is correct
4550
4294
 
4551
4295
  ---
4552
4296
 
4553
- ## 8. Quick Reference
4554
-
4555
- ### Platform Specs
4297
+ ## Examples
4556
4298
 
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 |
4299
+ **Quick pitch deck:**
4300
+ \`\`\`bash
4301
+ ${cmd2} create "SaaS analytics platform for e-commerce" --slides 8 --style professional
4302
+ \`\`\`
4563
4303
 
4564
- ### Timing (30fps)
4304
+ **From product brief:**
4305
+ \`\`\`bash
4306
+ ${cmd2} create --file brief.md --branding acme --output pitch.zip
4307
+ \`\`\`
4565
4308
 
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 |
4309
+ **Research presentation:**
4310
+ \`\`\`bash
4311
+ cat research-notes.txt | ${cmd2} create --slides 15 --style minimal
4312
+ \`\`\`
4313
+ `;
4314
+ }
4572
4315
 
4573
- ### Self-Check
4316
+ // src/commands/skill/installer.ts
4317
+ import { mkdirSync, writeFileSync, existsSync as existsSync2, rmSync } from "fs";
4318
+ import { join, resolve as resolve4, relative } from "path";
4319
+ import { homedir } from "os";
4574
4320
 
4575
- - [ ] 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
- }
4321
+ // src/commands/skill/editors.ts
4322
+ var SUPPORTED_EDITORS = [
4323
+ { name: "Claude Code", dir: ".claude" },
4324
+ { name: "Cursor", dir: ".cursor" },
4325
+ { name: "Codex", dir: ".codex" },
4326
+ { name: "OpenCode", dir: ".opencode" },
4327
+ { name: "Windsurf", dir: ".windsurf" },
4328
+ { name: "Agent", dir: ".agent" }
4585
4329
  ];
4586
4330
 
4587
4331
  // src/commands/skill/installer.ts
@@ -4594,27 +4338,10 @@ function validatePath(basePath, targetPath) {
4594
4338
  }
4595
4339
  return resolvedTarget;
4596
4340
  }
4597
- function getRuleFiles() {
4598
- const rules = [];
4599
- for (const rule of VIDEO_RULE_CONTENTS) {
4600
- rules.push({
4601
- path: `rules/video/${rule.filename}`,
4602
- content: rule.content
4603
- });
4604
- }
4605
- return rules;
4606
- }
4607
4341
  function installSkillToPath(skillPath, content) {
4608
4342
  const skillFile = join(skillPath, "SKILL.md");
4609
4343
  mkdirSync(skillPath, { recursive: true });
4610
4344
  writeFileSync(skillFile, content, "utf-8");
4611
- const rules = getRuleFiles();
4612
- for (const rule of rules) {
4613
- const rulePath = join(skillPath, rule.path);
4614
- const ruleDir = join(skillPath, rule.path.split("/").slice(0, -1).join("/"));
4615
- mkdirSync(ruleDir, { recursive: true });
4616
- writeFileSync(rulePath, rule.content, "utf-8");
4617
- }
4618
4345
  }
4619
4346
  function installSkill(skillName, content, options = {}) {
4620
4347
  const result = {
@@ -4678,67 +4405,113 @@ function getSupportedEditorNames() {
4678
4405
  }
4679
4406
 
4680
4407
  // src/commands/skill/index.ts
4681
- var skillCommand = new Command14("skill").description(`Manage ${brand.displayName} skill for AI coding assistants`).addHelpText(
4408
+ var skillContext = {
4409
+ name: brand.name,
4410
+ cmd: brand.commands[0],
4411
+ displayName: brand.displayName
4412
+ };
4413
+ var skillCommand = new Command14("skill").description(`Manage ${brand.displayName} skills for AI coding assistants`).addHelpText(
4682
4414
  "after",
4683
4415
  `
4684
4416
  ${chalk12.bold("Examples:")}
4685
- ${chalk12.gray("# Install skill for all detected editors")}
4686
- $ ${brand.name} skill install
4417
+ ${chalk12.gray("# Install video skill")}
4418
+ $ ${brand.commands[0]} skill install video
4419
+
4420
+ ${chalk12.gray("# Install presentation skill")}
4421
+ $ ${brand.commands[0]} skill install presentation
4422
+
4423
+ ${chalk12.gray("# Install both skills")}
4424
+ $ ${brand.commands[0]} skill install
4687
4425
 
4688
4426
  ${chalk12.gray("# Install to specific directory")}
4689
- $ ${brand.name} skill install --dir ~/.claude
4427
+ $ ${brand.commands[0]} skill install video --dir ~/.claude
4690
4428
 
4691
4429
  ${chalk12.gray("# Show skill content")}
4692
- $ ${brand.name} skill show
4430
+ $ ${brand.commands[0]} skill show video
4693
4431
  `
4694
4432
  );
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(", "));
4433
+ skillCommand.command("install").description(`Install ${brand.displayName} skills for AI coding assistants`).argument("[type]", "Skill type: video, presentation, or omit for both").option("-d, --dir <path>", "Install to specific directory").option("-g, --global", "Install globally (to home directory)", true).option("-l, --local", "Install locally (to current directory)").option("-f, --force", "Overwrite existing skill files").action(async (type, options) => {
4434
+ const skillsToInstall = [];
4435
+ if (!type || type === "video") {
4436
+ skillsToInstall.push({
4437
+ name: `${brand.name}-video`,
4438
+ content: generateVideoSkillContent(skillContext)
4439
+ });
4707
4440
  }
4708
- if (result.skipped.length > 0) {
4709
- console.log();
4710
- info(`Skipped (already exists): ${result.skipped.join(", ")}`);
4711
- console.log(chalk12.gray(" Use --force to overwrite"));
4441
+ if (!type || type === "presentation") {
4442
+ skillsToInstall.push({
4443
+ name: `${brand.name}-presentation`,
4444
+ content: generatePresentationSkillContent(skillContext)
4445
+ });
4712
4446
  }
4713
- if (result.errors.length > 0) {
4714
- console.log();
4715
- for (const err of result.errors) {
4716
- error(err);
4717
- }
4447
+ if (type && type !== "video" && type !== "presentation") {
4448
+ error(`Invalid skill type: ${type}. Must be "video" or "presentation"`);
4449
+ process.exit(1);
4718
4450
  }
4719
- if (result.installed.length === 0 && result.skipped.length === 0 && result.errors.length === 0) {
4720
- info("No supported AI coding assistants detected.");
4451
+ console.log();
4452
+ for (const skill of skillsToInstall) {
4453
+ info(`Installing ${skill.name}...`);
4454
+ const result = installSkill(skill.name, skill.content, {
4455
+ dir: options.dir,
4456
+ local: options.local,
4457
+ force: options.force
4458
+ });
4459
+ if (result.installed.length > 0) {
4460
+ success(`${skill.name} installed successfully`);
4461
+ keyValue(" Installed to", result.installed.join(", "));
4462
+ }
4463
+ if (result.skipped.length > 0) {
4464
+ info(` Skipped (already exists): ${result.skipped.join(", ")}`);
4465
+ console.log(chalk12.gray(" Use --force to overwrite"));
4466
+ }
4467
+ if (result.errors.length > 0) {
4468
+ for (const err of result.errors) {
4469
+ error(` ${err}`);
4470
+ }
4471
+ }
4472
+ if (result.installed.length === 0 && result.skipped.length === 0 && result.errors.length === 0) {
4473
+ info(" No supported AI coding assistants detected");
4474
+ console.log(chalk12.gray(" Supported editors: " + getSupportedEditorNames().join(", ")));
4475
+ console.log(chalk12.gray(" Use --dir <path> to install to a specific directory"));
4476
+ }
4721
4477
  console.log();
4722
- console.log(chalk12.gray("Supported editors: " + getSupportedEditorNames().join(", ")));
4723
- console.log(chalk12.gray("Use --dir <path> to install to a specific directory"));
4724
4478
  }
4725
- console.log();
4726
4479
  });
4727
- skillCommand.command("show").description("Display 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(", "));
4480
+ skillCommand.command("show").description("Display skill content").argument("[type]", "Skill type: video or presentation (default: video)").action((type = "video") => {
4481
+ if (type === "video") {
4482
+ console.log(generateVideoSkillContent(skillContext));
4483
+ } else if (type === "presentation") {
4484
+ console.log(generatePresentationSkillContent(skillContext));
4736
4485
  } else {
4737
- info("No installed skills found");
4486
+ error(`Invalid skill type: ${type}. Must be "video" or "presentation"`);
4487
+ process.exit(1);
4488
+ }
4489
+ });
4490
+ skillCommand.command("uninstall").description(`Remove ${brand.displayName} skills from AI coding assistants`).argument("[type]", "Skill type: video, presentation, or omit for both").option("-g, --global", "Uninstall globally (from home directory)", true).option("-l, --local", "Uninstall locally (from current directory)").action(async (type, options) => {
4491
+ const skillsToRemove = [];
4492
+ if (!type || type === "video") {
4493
+ skillsToRemove.push(`${brand.name}-video`);
4738
4494
  }
4739
- if (result.errors.length > 0) {
4740
- for (const err of result.errors) {
4741
- warn(`Failed to remove: ${err}`);
4495
+ if (!type || type === "presentation") {
4496
+ skillsToRemove.push(`${brand.name}-presentation`);
4497
+ }
4498
+ if (type && type !== "video" && type !== "presentation") {
4499
+ error(`Invalid skill type: ${type}. Must be "video" or "presentation"`);
4500
+ process.exit(1);
4501
+ }
4502
+ console.log();
4503
+ for (const skillName of skillsToRemove) {
4504
+ const result = uninstallSkill(skillName, { local: options.local });
4505
+ if (result.removed.length > 0) {
4506
+ success(`${skillName} uninstalled`);
4507
+ keyValue(" Removed from", result.removed.join(", "));
4508
+ } else {
4509
+ info(` ${skillName} not found`);
4510
+ }
4511
+ if (result.errors.length > 0) {
4512
+ for (const err of result.errors) {
4513
+ warn(` Failed to remove: ${err}`);
4514
+ }
4742
4515
  }
4743
4516
  }
4744
4517
  console.log();
@@ -5000,7 +4773,7 @@ async function downloadFile2(url, outputPath) {
5000
4773
  const buffer = await response.arrayBuffer();
5001
4774
  await writeFile4(outputPath, Buffer.from(buffer));
5002
4775
  }
5003
- var mixCommand = new Command17("create").description("Mix audio tracks into a video").requiredOption("--video <url>", "Input video file/URL").option("--music <url>", "Background music file/URL").option("--voice <url>", "Voiceover file/URL").option("--music-volume <percent>", "Music volume 0-100", "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) => {
4776
+ var mixCommand = new Command17("create").description("Mix audio tracks into a video (music will loop to match video duration)").requiredOption("--video <url>", "Input video file/URL").option("--music <url>", "Background music file/URL (will loop if shorter than video)").option("--voice <url>", "Voiceover file/URL").option("--music-volume <percent>", "Music volume 0-100 (default: 30, recommended for mix with voice)", "30").option("--voice-volume <percent>", "Voice volume 0-100", "100").option("-o, --output <path>", "Output file path").option("--no-wait", "Do not wait for completion").option("-f, --format <format>", "Output format: human, json, quiet", "human").action(async (options) => {
5004
4777
  if (!options.music && !options.voice) {
5005
4778
  error("At least one of --music or --voice must be provided");
5006
4779
  process.exit(EXIT_CODES.INVALID_INPUT);
@@ -5169,9 +4942,10 @@ init_output();
5169
4942
  init_types();
5170
4943
  import { Command as Command19 } from "commander";
5171
4944
  import ora12 from "ora";
5172
- import { mkdir, writeFile as writeFile5, readFile as readFile2, access, rm } from "fs/promises";
4945
+ import { mkdir, writeFile as writeFile5, readFile as readFile2, access, rm, cp } from "fs/promises";
5173
4946
  import { join as join2, resolve as resolve5 } from "path";
5174
4947
  import { execSync, spawn } from "child_process";
4948
+ import ffmpegPath from "ffmpeg-static";
5175
4949
  var DEFAULT_TEMPLATE = "inizio-inc/remotion-composition";
5176
4950
  var DEFAULT_FPS = 30;
5177
4951
  function parseScriptIntoSections(script) {
@@ -5310,7 +5084,7 @@ function getExtension(url) {
5310
5084
  }
5311
5085
  return "jpg";
5312
5086
  }
5313
- var 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) => {
5087
+ var createCommand2 = new Command19("create").description("Create video assets (voiceover per scene, music, images)").option("-s, --script <text>", "Narration script (legacy single-script mode)").option("--script-file <path>", "Path to script file (legacy) or scenes JSON").option("-t, --topic <text>", "Topic for image search").option("-v, --voice <name>", "TTS voice (Kore, Puck, Rachel, alloy)", "Kore").option("-m, --music-prompt <text>", "Music description").option("-n, --num-images <number>", "Number of images to search/download", "5").option("-o, --output <dir>", "Output directory", "./public").option("-f, --format <format>", "Output format: human, json, quiet", "human").action(async (options) => {
5314
5088
  const format = options.format;
5315
5089
  const spinner = format === "human" ? ora12("Initializing...").start() : null;
5316
5090
  try {
@@ -5362,7 +5136,10 @@ var createCommand3 = new Command19("create").description("Create video assets (v
5362
5136
  if (spinner) spinner.text = `[${scene.name}] Generating speech...`;
5363
5137
  const ttsResult = await generateSpeech({
5364
5138
  text: scene.script,
5365
- options: { voice }
5139
+ options: {
5140
+ voice,
5141
+ voiceSettings: scenesInput.voiceSettings
5142
+ }
5366
5143
  });
5367
5144
  const audioPath = join2(audioDir, `${filename}.${ttsResult.format}`);
5368
5145
  await writeFile5(audioPath, ttsResult.audioData);
@@ -5417,7 +5194,7 @@ var createCommand3 = new Command19("create").description("Create video assets (v
5417
5194
  try {
5418
5195
  const videoResults = await searchVideos({
5419
5196
  query: scene.videoQuery,
5420
- options: { maxResults: 1 }
5197
+ options: { maxResults: 1, license: "free" }
5421
5198
  });
5422
5199
  const vids = videoResults.data.results.flatMap((r) => r.results);
5423
5200
  totalCost += videoResults.data.totalCost;
@@ -5502,7 +5279,12 @@ var createCommand3 = new Command19("create").description("Create video assets (v
5502
5279
  spinner?.start();
5503
5280
  }
5504
5281
  }
5505
- const musicDuration = Math.min(30, Math.ceil(totalDuration) + 5);
5282
+ const musicDuration = Math.min(30, Math.ceil(totalDuration));
5283
+ console.log(`[Music Generation] Requesting music:`, {
5284
+ prompt: musicPrompt,
5285
+ requestedDuration: musicDuration,
5286
+ totalAudioDuration: totalDuration
5287
+ });
5506
5288
  if (spinner) spinner.text = "Generating music...";
5507
5289
  let musicResult = await generateMusic({
5508
5290
  prompt: musicPrompt,
@@ -5526,15 +5308,28 @@ var createCommand3 = new Command19("create").description("Create video assets (v
5526
5308
  await downloadFile3(musicResult.audioUrl, musicPath);
5527
5309
  }
5528
5310
  totalCost += musicResult.cost || 0;
5311
+ const actualMusicDuration = musicResult.duration || musicDuration;
5312
+ console.log(`[Music Generation] Received music:`, {
5313
+ requestedDuration: musicDuration,
5314
+ returnedDuration: musicResult.duration,
5315
+ actualUsedDuration: actualMusicDuration,
5316
+ totalAudioDuration: totalDuration,
5317
+ difference: actualMusicDuration - totalDuration,
5318
+ audioUrl: musicResult.audioUrl?.substring(0, 50) + "..."
5319
+ });
5529
5320
  const musicInfo = {
5530
5321
  path: "audio/music.mp3",
5531
- duration: musicResult.duration || musicDuration,
5322
+ duration: actualMusicDuration,
5532
5323
  prompt: musicPrompt,
5533
5324
  cost: musicResult.cost || 0
5534
5325
  };
5535
5326
  if (format === "human") {
5536
5327
  spinner?.stop();
5537
5328
  success(`Music: ${musicPath} (${musicInfo.duration}s)`);
5329
+ if (actualMusicDuration < totalDuration) {
5330
+ warn(`Music duration (${actualMusicDuration.toFixed(1)}s) is shorter than video duration (${totalDuration.toFixed(1)}s).`);
5331
+ warn(`Consider using audio looping or extending music in Remotion.`);
5332
+ }
5538
5333
  spinner?.start();
5539
5334
  }
5540
5335
  if (spinner) spinner.text = "Writing manifest...";
@@ -5582,7 +5377,7 @@ var createCommand3 = new Command19("create").description("Create video assets (v
5582
5377
  process.exit(EXIT_CODES.GENERAL_ERROR);
5583
5378
  }
5584
5379
  });
5585
- var searchCommand2 = new Command19("search").description("Search for stock videos").argument("<query>", "Search query").option("-n, --max-results <count>", "Maximum number of results", "10").option("-o, --orientation <type>", "Video orientation: landscape, portrait, square, any", "any").option("-l, --license <type>", "License type: free, premium, any", "any").option("-f, --format <format>", "Output format: human, json, quiet", "human").action(async (query, options) => {
5380
+ var searchCommand2 = new Command19("search").description("Search for stock videos").argument("<query>", "Search query").option("-n, --max-results <count>", "Maximum number of results", "10").option("-o, --orientation <type>", "Video orientation: landscape, portrait, square, any", "any").option("-l, --license <type>", "License type: free, premium, any", "free").option("-f, --format <format>", "Output format: human, json, quiet", "human").action(async (query, options) => {
5586
5381
  const { maxResults, orientation, license, format } = options;
5587
5382
  const spinner = format === "human" ? ora12("Searching for videos...").start() : null;
5588
5383
  try {
@@ -5751,10 +5546,147 @@ var initCommand = new Command19("init").description("Create a new Remotion video
5751
5546
  process.exit(EXIT_CODES.GENERAL_ERROR);
5752
5547
  }
5753
5548
  });
5754
- var videoCommand = new Command19("video").description("Video asset generation commands").addCommand(initCommand).addCommand(createCommand3).addCommand(searchCommand2);
5549
+ function getFfmpegPath() {
5550
+ if (!ffmpegPath) {
5551
+ throw new Error("ffmpeg-static binary not found. Try reinstalling the CLI.");
5552
+ }
5553
+ return ffmpegPath;
5554
+ }
5555
+ var thumbnailCommand = new Command19("thumbnail").description("Embed a thumbnail/poster image into a video file for preview in Slack, Twitter, etc.").argument("<video>", "Video file to add thumbnail to (e.g., out/video.mp4)").option("-i, --image <path>", "Thumbnail image to embed (if not provided, extracts from video)").option("-f, --frame <number>", "Frame number to extract as thumbnail (default: 30)", "30").option("-c, --composition <id>", "Remotion composition to extract frame from (uses Remotion still)").option("-o, --output <path>", "Output video path (default: overwrites input)").option("--json", "Output as JSON").option("-q, --quiet", "Only output the file path").action(async (videoPath, options) => {
5556
+ const format = options.json ? "json" : options.quiet ? "quiet" : "human";
5557
+ const spinner = format === "human" ? ora12("Processing...").start() : null;
5558
+ try {
5559
+ let ffmpeg;
5560
+ try {
5561
+ ffmpeg = getFfmpegPath();
5562
+ } catch (err) {
5563
+ spinner?.stop();
5564
+ error(err instanceof Error ? err.message : "ffmpeg not available");
5565
+ process.exit(EXIT_CODES.GENERAL_ERROR);
5566
+ }
5567
+ const videoFullPath = resolve5(process.cwd(), videoPath);
5568
+ try {
5569
+ await access(videoFullPath);
5570
+ } catch {
5571
+ spinner?.stop();
5572
+ error(`Video file not found: ${videoPath}`);
5573
+ process.exit(EXIT_CODES.INVALID_INPUT);
5574
+ }
5575
+ const frameNum = parseInt(options.frame, 10);
5576
+ if (isNaN(frameNum) || frameNum < 0) {
5577
+ spinner?.stop();
5578
+ error("Invalid frame number. Must be a non-negative integer.");
5579
+ process.exit(EXIT_CODES.INVALID_INPUT);
5580
+ }
5581
+ let thumbnailPath = options.image;
5582
+ let tempThumbnail = false;
5583
+ if (!thumbnailPath) {
5584
+ const tempDir = join2(process.cwd(), ".tmp-thumbnail");
5585
+ await mkdir(tempDir, { recursive: true });
5586
+ thumbnailPath = join2(tempDir, "thumb.png");
5587
+ tempThumbnail = true;
5588
+ if (options.composition) {
5589
+ if (spinner) spinner.text = `Extracting frame ${frameNum} from ${options.composition}...`;
5590
+ const args = [
5591
+ "exec",
5592
+ "remotion",
5593
+ "still",
5594
+ options.composition,
5595
+ thumbnailPath,
5596
+ `--frame=${frameNum}`
5597
+ ];
5598
+ try {
5599
+ execSync(`pnpm ${args.join(" ")}`, {
5600
+ stdio: "pipe",
5601
+ cwd: process.cwd()
5602
+ });
5603
+ } catch (err) {
5604
+ spinner?.stop();
5605
+ error(`Failed to extract frame from composition: ${err instanceof Error ? err.message : "Unknown error"}`);
5606
+ process.exit(EXIT_CODES.GENERAL_ERROR);
5607
+ }
5608
+ } else {
5609
+ if (spinner) spinner.text = `Extracting frame ${frameNum} from video...`;
5610
+ try {
5611
+ execSync(
5612
+ `"${ffmpeg}" -y -i "${videoFullPath}" -vf "select=eq(n\\,${frameNum})" -vframes 1 "${thumbnailPath}"`,
5613
+ { stdio: "pipe" }
5614
+ );
5615
+ } catch (err) {
5616
+ spinner?.stop();
5617
+ error(`Failed to extract frame from video: ${err instanceof Error ? err.message : "Unknown error"}`);
5618
+ process.exit(EXIT_CODES.GENERAL_ERROR);
5619
+ }
5620
+ }
5621
+ } else {
5622
+ thumbnailPath = resolve5(process.cwd(), thumbnailPath);
5623
+ try {
5624
+ await access(thumbnailPath);
5625
+ } catch {
5626
+ spinner?.stop();
5627
+ error(`Thumbnail image not found: ${options.image}`);
5628
+ process.exit(EXIT_CODES.INVALID_INPUT);
5629
+ }
5630
+ }
5631
+ const outputPath = options.output ? resolve5(process.cwd(), options.output) : videoFullPath;
5632
+ const needsTempOutput = outputPath === videoFullPath;
5633
+ const tempOutput = needsTempOutput ? videoFullPath.replace(/\.mp4$/, ".thumb-temp.mp4") : outputPath;
5634
+ if (spinner) spinner.text = "Embedding thumbnail into video...";
5635
+ try {
5636
+ execSync(
5637
+ `"${ffmpeg}" -y -i "${videoFullPath}" -i "${thumbnailPath}" -map 0 -map 1 -c copy -disposition:v:1 attached_pic "${tempOutput}"`,
5638
+ { stdio: "pipe" }
5639
+ );
5640
+ if (needsTempOutput) {
5641
+ await rm(videoFullPath);
5642
+ await cp(tempOutput, videoFullPath);
5643
+ await rm(tempOutput);
5644
+ }
5645
+ } catch (err) {
5646
+ spinner?.stop();
5647
+ error(`Failed to embed thumbnail: ${err instanceof Error ? err.message : "Unknown error"}`);
5648
+ process.exit(EXIT_CODES.GENERAL_ERROR);
5649
+ }
5650
+ if (tempThumbnail) {
5651
+ try {
5652
+ await rm(join2(process.cwd(), ".tmp-thumbnail"), { recursive: true });
5653
+ } catch {
5654
+ }
5655
+ }
5656
+ spinner?.stop();
5657
+ const finalOutput = options.output || videoPath;
5658
+ if (format === "json") {
5659
+ printJson({
5660
+ video: finalOutput,
5661
+ thumbnail: options.image || `frame ${frameNum}`,
5662
+ composition: options.composition || null
5663
+ });
5664
+ return;
5665
+ }
5666
+ if (format === "quiet") {
5667
+ console.log(resolve5(process.cwd(), finalOutput));
5668
+ return;
5669
+ }
5670
+ console.log();
5671
+ success(`Thumbnail embedded: ${finalOutput}`);
5672
+ if (options.image) {
5673
+ keyValue("Thumbnail", options.image);
5674
+ } else if (options.composition) {
5675
+ keyValue("Source", `${options.composition} frame ${frameNum}`);
5676
+ } else {
5677
+ keyValue("Source", `Video frame ${frameNum}`);
5678
+ }
5679
+ console.log();
5680
+ } catch (err) {
5681
+ spinner?.stop();
5682
+ error(err instanceof Error ? err.message : "Failed to process thumbnail");
5683
+ process.exit(EXIT_CODES.GENERAL_ERROR);
5684
+ }
5685
+ });
5686
+ var videoCommand = new Command19("video").description("Video asset generation commands").addCommand(initCommand).addCommand(createCommand2).addCommand(searchCommand2).addCommand(thumbnailCommand);
5755
5687
 
5756
5688
  // src/index.ts
5757
- var VERSION = "0.1.7";
5689
+ var VERSION = "0.1.8";
5758
5690
  var program = new Command20();
5759
5691
  var cmdName = brand.commands[0];
5760
5692
  program.name(cmdName).description(brand.description).version(VERSION, "-v, --version", "Show version number").option("--debug", "Enable debug logging").option("--no-color", "Disable colored output").configureOutput({