@conceptcraft/mindframes 0.1.6 → 0.1.8

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