@conceptcraft/mindframes 0.1.6 → 0.1.7

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
@@ -1025,7 +1025,7 @@ async function pollForCompletion(checkFn, maxAttempts = 60, intervalMs = 2e3) {
1025
1025
  if (result.status === "completed" || result.status === "failed") {
1026
1026
  return result;
1027
1027
  }
1028
- await new Promise((resolve5) => setTimeout(resolve5, intervalMs));
1028
+ await new Promise((resolve6) => setTimeout(resolve6, intervalMs));
1029
1029
  }
1030
1030
  throw new ApiError("Operation timed out", 408, 1);
1031
1031
  }
@@ -1132,10 +1132,10 @@ function generateState() {
1132
1132
  async function findAvailablePort(start, end) {
1133
1133
  for (let port = start; port <= end; port++) {
1134
1134
  try {
1135
- await new Promise((resolve5, reject) => {
1135
+ await new Promise((resolve6, reject) => {
1136
1136
  const server = http.createServer();
1137
1137
  server.listen(port, () => {
1138
- server.close(() => resolve5());
1138
+ server.close(() => resolve6());
1139
1139
  });
1140
1140
  server.on("error", reject);
1141
1141
  });
@@ -1193,7 +1193,7 @@ async function exchangeCodeForTokens(tokenEndpoint, code, codeVerifier, redirect
1193
1193
  return response.json();
1194
1194
  }
1195
1195
  function startCallbackServer(port, expectedState) {
1196
- return new Promise((resolve5, reject) => {
1196
+ return new Promise((resolve6, reject) => {
1197
1197
  let timeoutId;
1198
1198
  let settled = false;
1199
1199
  const cleanup = () => {
@@ -1279,7 +1279,7 @@ function startCallbackServer(port, expectedState) {
1279
1279
  </html>
1280
1280
  `);
1281
1281
  cleanup();
1282
- resolve5({ code, state });
1282
+ resolve6({ code, state });
1283
1283
  });
1284
1284
  server.listen(port);
1285
1285
  process.once("SIGINT", onCancel);
@@ -2422,21 +2422,21 @@ Uploading ${options.file.length} file(s)...`));
2422
2422
  }
2423
2423
  });
2424
2424
  async function readStdin() {
2425
- return new Promise((resolve5) => {
2425
+ return new Promise((resolve6) => {
2426
2426
  let data = "";
2427
2427
  process.stdin.setEncoding("utf8");
2428
2428
  if (process.stdin.isTTY) {
2429
- resolve5("");
2429
+ resolve6("");
2430
2430
  return;
2431
2431
  }
2432
2432
  process.stdin.on("data", (chunk) => {
2433
2433
  data += chunk;
2434
2434
  });
2435
2435
  process.stdin.on("end", () => {
2436
- resolve5(data.trim());
2436
+ resolve6(data.trim());
2437
2437
  });
2438
2438
  setTimeout(() => {
2439
- resolve5(data.trim());
2439
+ resolve6(data.trim());
2440
2440
  }, 100);
2441
2441
  });
2442
2442
  }
@@ -3127,170 +3127,197 @@ var whoamiCommand = new Command13("whoami").description("Show current user and t
3127
3127
  }
3128
3128
  });
3129
3129
 
3130
- // src/commands/skill.ts
3130
+ // src/commands/skill/index.ts
3131
3131
  init_output();
3132
3132
  import { Command as Command14 } from "commander";
3133
3133
  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
3134
 
3152
- # ${name} CLI
3135
+ // src/commands/skill/sections/frontmatter.ts
3136
+ var frontmatter = {
3137
+ title: "Frontmatter",
3138
+ render: (ctx) => `---
3139
+ name: ${ctx.cmd}
3140
+ description: Use when user asks to create presentations (slides, decks, pitch decks) or videos (product demos, explainers, social content, promos). Also handles voiceover and music generation.
3141
+ ---`
3142
+ };
3153
3143
 
3154
- Create professional presentations and videos directly from your terminal.
3144
+ // src/commands/skill/sections/header.ts
3145
+ var header3 = {
3146
+ title: "Header",
3147
+ render: (ctx) => `# ${ctx.name} CLI
3155
3148
 
3156
- ## Prerequisites
3149
+ Create professional presentations directly from your terminal. The CLI generates AI-powered slides from any context you provide - text, files, URLs, or piped content.`
3150
+ };
3151
+
3152
+ // src/commands/skill/sections/prerequisites.ts
3153
+ var prerequisites = {
3154
+ title: "Prerequisites",
3155
+ render: (ctx) => `## Prerequisites
3157
3156
 
3158
3157
  \`\`\`bash
3159
- npm install -g ${pkg}
3160
- ${cmd2} login # Authenticate (opens browser)
3161
- ${cmd2} whoami # Verify setup
3158
+ npm install -g ${ctx.pkg}
3159
+ ${ctx.cmd} login # Authenticate (opens browser)
3160
+ ${ctx.cmd} whoami # Verify auth
3162
3161
  \`\`\`
3163
3162
 
3164
- ## Rules
3163
+ ### Authentication
3165
3164
 
3166
- Read these for detailed usage:
3165
+ If not authenticated, run \`${ctx.cmd} login\` - it opens browser for OAuth.
3166
+ If login fails or user declines, fall back to API key: \`${ctx.cmd} config init\``
3167
+ };
3167
3168
 
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
3169
+ // src/commands/skill/sections/workflow.ts
3170
+ var workflow = {
3171
+ title: "Core Workflow",
3172
+ render: (ctx) => `## Core Workflow
3173
3173
 
3174
- ## Quick Reference
3174
+ 1. **Gather context** - Read relevant files, code, or documentation
3175
+ 2. **Create presentation** - Pass context to \`${ctx.cmd} create\`
3176
+ 3. **Share URL** - Return the presentation link to the user`
3177
+ };
3175
3178
 
3176
- ### Presentations
3179
+ // src/commands/skill/sections/create-command.ts
3180
+ var createCommand2 = {
3181
+ title: "Create Command",
3182
+ render: (ctx) => `## Commands
3177
3183
 
3178
- \`\`\`bash
3179
- # Create from context
3180
- cat README.md | ${cmd2} create "Project Overview"
3184
+ ### Create Presentation
3181
3185
 
3182
- # With files
3183
- ${cmd2} create "Product Demo" --file ./deck.pptx --file ./logo.png
3186
+ Context is **required**. Provide it via one of these methods:
3184
3187
 
3185
- # With options
3186
- ${cmd2} create "API Docs" --slides 8 --tone educational --goal inform
3187
- \`\`\`
3188
+ \`\`\`bash
3189
+ # Upload files (PDFs, PPTX, images, docs)
3190
+ ${ctx.cmd} create "Product Overview" --file ./deck.pptx --file ./logo.png
3188
3191
 
3189
- ### Video Creation
3192
+ # Direct text context
3193
+ ${ctx.cmd} create "Topic Title" --context "Key points, data, facts..."
3190
3194
 
3191
- \`\`\`bash
3192
- # Scaffold project
3193
- ${cmd2} video init my-video
3195
+ # From a text file
3196
+ ${ctx.cmd} create "Topic Title" --context-file ./notes.md
3194
3197
 
3195
- # Generate all assets (voiceover, music, images)
3196
- ${cmd2} video create --script "Your narration..." --output ./public
3198
+ # Pipe content (auto-detected)
3199
+ cat README.md | ${ctx.cmd} create "Project Overview"
3197
3200
 
3198
- # Search for stock content
3199
- ${cmd2} image search -q "tropical beach" -n 5
3200
- ${cmd2} video search "tech workspace" -n 5
3201
- \`\`\`
3201
+ # From URLs (scraped automatically)
3202
+ ${ctx.cmd} create "Competitor Analysis" --sources https://example.com/report
3202
3203
 
3203
- ### Audio Generation
3204
+ # Combine multiple sources
3205
+ cat src/auth/*.ts | ${ctx.cmd} create "Auth System" \\
3206
+ --file ./architecture.png \\
3207
+ --context "Focus on security patterns"
3208
+ \`\`\``
3209
+ };
3204
3210
 
3205
- \`\`\`bash
3206
- # Text-to-speech
3207
- ${cmd2} tts generate -t "Narration text" -o voice.wav --voice Kore
3211
+ // src/commands/skill/sections/create-options.ts
3212
+ var createOptions = {
3213
+ title: "Create Options",
3214
+ render: (_ctx) => `### Create Options
3208
3215
 
3209
- # Music generation
3210
- ${cmd2} music generate -p "uplifting corporate" -d 30 -o music.mp3
3216
+ | Option | Description | Default |
3217
+ |--------|-------------|---------|
3218
+ | \`-n, --slides <count>\` | Number of slides (1-20) | 10 |
3219
+ | \`-m, --mode <mode>\` | Quality: \`instant\`, \`fast\`, \`balanced\`, \`best\` | balanced |
3220
+ | \`-t, --tone <tone>\` | Tone: \`professional\`, \`educational\`, \`creative\`, \`formal\`, \`casual\` | professional |
3221
+ | \`--amount <amount>\` | Density: \`minimal\`, \`concise\`, \`detailed\`, \`extensive\` | concise |
3222
+ | \`--audience <text>\` | Target audience | General Audience |
3223
+ | \`-g, --goal <type>\` | Purpose: \`inform\`, \`persuade\`, \`train\`, \`learn\`, \`entertain\`, \`report\` | - |
3224
+ | \`--custom-goal <text>\` | Custom goal description | - |
3225
+ | \`-f, --file <paths...>\` | Files to upload (PDF, PPTX, images, docs) | - |
3226
+ | \`-l, --language <lang>\` | Output language | en |
3227
+ | \`-b, --brand <id>\` | Branding ID to apply | - |
3228
+ | \`-o, --output <format>\` | Output: \`human\`, \`json\`, \`quiet\` | human |`
3229
+ };
3211
3230
 
3212
- # Mix audio into video
3213
- ${cmd2} mix create --video video.mp4 --voice voice.wav --music music.mp3 -o final.mp4
3214
- \`\`\`
3231
+ // src/commands/skill/sections/other-commands.ts
3232
+ var otherCommands = {
3233
+ title: "Other Commands",
3234
+ render: (ctx) => `### Other Commands
3215
3235
 
3216
- ## Assets
3236
+ \`\`\`bash
3237
+ # Check authentication
3238
+ ${ctx.cmd} whoami
3217
3239
 
3218
- Copy animation components from \`assets/animation-components.tsx\` for Remotion videos.
3240
+ # List presentations
3241
+ ${ctx.cmd} list
3242
+ ${ctx.cmd} list --format json
3219
3243
 
3220
- ## Asking Questions
3244
+ # Get presentation details
3245
+ ${ctx.cmd} get <id-or-slug>
3221
3246
 
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
- ---
3247
+ # Export to ZIP
3248
+ ${ctx.cmd} export <id-or-slug> -o presentation.zip
3232
3249
 
3233
- # Presentations
3250
+ # Import presentation
3251
+ ${ctx.cmd} import ./presentation.zip
3234
3252
 
3235
- ## Workflow
3253
+ # Manage branding
3254
+ ${ctx.cmd} branding list
3255
+ ${ctx.cmd} branding extract https://company.com
3236
3256
 
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
3257
+ # Install/manage this skill
3258
+ ${ctx.cmd} skill install
3259
+ ${ctx.cmd} skill show
3260
+ \`\`\``
3261
+ };
3240
3262
 
3241
- ## Create Command
3263
+ // src/commands/skill/sections/examples.ts
3264
+ var examples = {
3265
+ title: "Examples",
3266
+ render: (ctx) => `## Examples
3242
3267
 
3243
- Context is **required**. Provide via:
3268
+ ### Present a Codebase Feature
3244
3269
 
3245
3270
  \`\`\`bash
3246
- # Upload files (PDFs, PPTX, images, docs)
3247
- ${cmd2} create "Product Overview" --file ./deck.pptx --file ./logo.png
3248
-
3249
- # Direct text context
3250
- ${cmd2} create "Topic Title" --context "Key points, data, facts..."
3271
+ # Read the relevant files and create presentation
3272
+ cat src/lib/auth.ts src/lib/session.ts | ${ctx.cmd} create "Authentication System" \\
3273
+ --slides 8 --tone educational --audience "New developers" \\
3274
+ --goal train
3275
+ \`\`\`
3251
3276
 
3252
- # From a text file
3253
- ${cmd2} create "Topic Title" --context-file ./notes.md
3277
+ ### Technical Documentation with Diagrams
3254
3278
 
3255
- # Pipe content
3256
- cat README.md | ${cmd2} create "Project Overview"
3279
+ \`\`\`bash
3280
+ ${ctx.cmd} create "API Reference" \\
3281
+ --file ./docs/api.md \\
3282
+ --file ./diagrams/architecture.png \\
3283
+ --mode best --amount detailed \\
3284
+ --goal inform
3285
+ \`\`\`
3257
3286
 
3258
- # From URLs
3259
- ${cmd2} create "Competitor Analysis" --sources https://example.com/report
3287
+ ### Quick Project Overview
3260
3288
 
3261
- # Combine sources
3262
- cat src/auth/*.ts | ${cmd2} create "Auth System" \\
3263
- --file ./architecture.png \\
3264
- --context "Focus on security patterns"
3289
+ \`\`\`bash
3290
+ cat README.md package.json | ${ctx.cmd} create "Project Introduction" \\
3291
+ -m instant --slides 5
3265
3292
  \`\`\`
3266
3293
 
3267
- ## Options
3294
+ ### Sales Deck from Existing Presentation
3268
3295
 
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 | - |
3296
+ \`\`\`bash
3297
+ ${ctx.cmd} create "Product Demo" \\
3298
+ --file ./existing-deck.pptx \\
3299
+ --goal persuade \\
3300
+ --audience "Enterprise buyers" \\
3301
+ --tone professional
3302
+ \`\`\`
3280
3303
 
3281
- ## Other Commands
3304
+ ### Research Presentation
3282
3305
 
3283
3306
  \`\`\`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
- \`\`\`
3307
+ ${ctx.cmd} create "Market Analysis" \\
3308
+ --file ./research.pdf \\
3309
+ --sources https://report.com/industry.pdf \\
3310
+ --tone formal --audience "Executive team" \\
3311
+ --goal report
3312
+ \`\`\``
3313
+ };
3291
3314
 
3292
- ## Output
3315
+ // src/commands/skill/sections/output.ts
3316
+ var output = {
3317
+ title: "Output",
3318
+ render: (ctx) => `## Output
3293
3319
 
3320
+ Successful creation returns:
3294
3321
  \`\`\`
3295
3322
  \u2713 Presentation created successfully
3296
3323
 
@@ -3298,1581 +3325,1424 @@ ${cmd2} branding extract https://... # Extract branding from URL
3298
3325
  Slides: 8
3299
3326
  Generated in: 45s \xB7 12,500 tokens
3300
3327
 
3301
- Open: ${url}/en/view/presentations/auth-system-v1-abc123
3328
+ Open: ${ctx.url}/en/view/presentations/auth-system-v1-abc123
3302
3329
  \`\`\`
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
3310
- ---
3311
-
3312
- # Video Creation
3313
3330
 
3314
- **Replicate the app's UI AS CLOSELY AS POSSIBLE - almost an exact copy.**
3315
-
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.
3331
+ For scripting, use JSON output:
3332
+ \`\`\`bash
3333
+ URL=$(${ctx.cmd} create "Demo" --context "..." -o json | jq -r '.viewUrl')
3334
+ \`\`\``
3335
+ };
3317
3336
 
3318
- ---
3337
+ // src/commands/skill/sections/best-practices.ts
3338
+ var bestPractices = {
3339
+ title: "Best Practices",
3340
+ render: (_ctx) => `## Best Practices
3341
+
3342
+ 1. **Provide rich context** - More context = better slides. Include code, docs, data.
3343
+ 2. **Use file uploads for binary content** - PDFs, images, PPTX files need \`--file\`.
3344
+ 3. **Specify a goal** - Helps tailor the presentation structure and messaging.
3345
+ 4. **Use appropriate mode** - \`instant\` for quick drafts, \`best\` for important presentations.
3346
+ 5. **Specify audience** - Helps tailor complexity and terminology.
3347
+ 6. **Combine sources** - Pipe multiple files for comprehensive presentations.`
3348
+ };
3319
3349
 
3320
- ## \u26D4 HARD RULES
3350
+ // src/commands/skill/sections/file-types.ts
3351
+ var fileTypes = {
3352
+ title: "Supported File Types",
3353
+ render: (_ctx) => `## Supported File Types
3321
3354
 
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\`.
3355
+ - **Documents**: PDF, DOCX, XLSX, PPTX
3356
+ - **Images**: JPEG, PNG, GIF, WebP
3357
+ - **Text**: Markdown, TXT, CSV, JSON`
3358
+ };
3327
3359
 
3328
- ---
3360
+ // src/commands/skill/sections/troubleshooting.ts
3361
+ var troubleshooting = {
3362
+ title: "Troubleshooting",
3363
+ render: (ctx) => `## Troubleshooting
3329
3364
 
3330
- ## \u{1F534} PHASE 0: READ REFERENCES FIRST
3365
+ \`\`\`bash
3366
+ # Check if authenticated
3367
+ ${ctx.cmd} whoami
3331
3368
 
3332
- **Before doing ANYTHING, read these files:**
3369
+ # Re-authenticate if needed
3370
+ ${ctx.cmd} login
3333
3371
 
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
3372
+ # Debug mode
3373
+ ${ctx.cmd} create "Test" --context "test" --debug
3374
+ \`\`\``
3375
+ };
3339
3376
 
3340
- ---
3377
+ // src/commands/skill/sections/video-creation.ts
3378
+ var videoCreation = {
3379
+ title: "Video Creation",
3380
+ render: (ctx) => `## Video Creation
3341
3381
 
3342
- ## \u{1F3AF} TWO VIDEO MODES
3382
+ ### How to Create a Perfect Video
3343
3383
 
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
3384
+ Videos must feel like premium tech launches (Stripe, Apple, Linear) - **Kinetic Composition**, not slideshows.
3349
3385
 
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
3386
+ **Core Philosophy:** "Nothing sits still. Everything is physics-based. Every pixel breathes."
3355
3387
 
3356
- **DEFAULT TO MODE A if user mentions their app/project.**
3388
+ ### Required Reading (The Motion Bible)
3357
3389
 
3358
- ---
3390
+ | Rule | Why This Matters |
3391
+ |------|------------------|
3392
+ | [video-creation-guide.md](rules/video/video-creation-guide.md) | **The Motion Bible.** Camera rigs, 2.5D entrances, cursor physics, typography, backgrounds |
3393
+ | [failures.md](rules/video/failures.md) | **Avoid rejection.** 7 common mistakes that make videos look like slideshows |
3394
+ | [project-based.md](rules/video/project-based.md) | **Copy real components.** Eject actual UI code, strip logic, animate pixel-perfect |
3395
+ | [parameterization.md](rules/video/parameterization.md) | **Saves debugging.** Never hardcode frame numbers |
3396
+ | [layers.md](rules/video/layers.md) | **Prevents z-index bugs.** Background orbs \u2192 Vignette \u2192 CameraRig \u2192 Content |
3397
+ | [social-media.md](rules/video/social-media.md) | **Platform specs.** Resolution, aspect ratio, duration per platform |
3359
3398
 
3360
- ## Pre-Creation Questions
3399
+ **Required:** \`npx skills add https://github.com/remotion-dev/skills --skill remotion-best-practices\`
3361
3400
 
3362
- Before creating a video, use \`AskUserQuestion\` tool (if available) to ask:
3401
+ ### Workflow
3363
3402
 
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
- }
3401
- \`\`\`
3403
+ #### Phase 1: Discovery
3402
3404
 
3403
- If \`AskUserQuestion\` tool is not available, ask these questions in text format.
3405
+ Explore the project thoroughly - assets, components, branding, what makes this product/topic unique.
3404
3406
 
3405
- ## Audio-First Workflow
3407
+ #### Phase 2: Video Brief
3406
3408
 
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.
3409
+ Present a brief outline (scenes, duration, assets found) and get user approval before production.
3408
3410
 
3409
- ### Step 1: Write Script
3411
+ #### Phase 3: Production
3410
3412
 
3411
- Write narration for the target duration. Structure: Hook \u2192 Key points \u2192 CTA
3413
+ 1. **Read video-creation-guide.md + project-based.md + remotion-best-practices**
3414
+ 2. **Generate audio** - \`${ctx.cmd} video create\` with scenes JSON
3415
+ 3. **Scaffold OUTSIDE project** - \`cd .. && ${ctx.cmd} video init my-video\`
3416
+ 4. **Copy assets** - Logo, fonts, colors from project into video project
3417
+ 5. **Implement kinetically** - Camera rig, 2.5D entrances, staggered lists, transitions
3418
+ 6. **Verify checklist** - Is it kinetic or static?
3412
3419
 
3413
- Tip: ~2.5 words per second for natural pacing.
3420
+ #### Phase 4: Render
3414
3421
 
3415
- ### Step 2: Generate Assets (Audio-First)
3422
+ Auto-render when done: \`pnpm exec remotion render FullVideo\`
3416
3423
 
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
- \`\`\`
3424
+ Only open Studio if user asks.
3426
3425
 
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**
3426
+ ### Kinetic Checklist
3432
3427
 
3433
- ### Step 3: Read Manifest Sections
3428
+ - [ ] CameraRig wraps entire scene with drift/zoom
3429
+ - [ ] Every UI element uses 2.5D rotation entrance (\`perspective + rotateX\`)
3430
+ - [ ] Cursor moves in Bezier curves with overshoot
3431
+ - [ ] Lists/grids stagger (never appear all at once)
3432
+ - [ ] Text uses masked reveal or keyword animation
3433
+ - [ ] Background has moving orbs + vignette + noise
3434
+ - [ ] Something is moving on EVERY frame
3435
+ - [ ] No static resting states longer than 30 frames
3434
3436
 
3435
- The manifest includes a \`sections\` array with **exact timing from TTS character-level timestamps**:
3437
+ ### Asset Generation
3436
3438
 
3437
- \`\`\`json
3439
+ \`\`\`bash
3440
+ cat <<EOF | ${ctx.cmd} video create --output ./public
3438
3441
  {
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
- }
3442
+ "scenes": [
3443
+ { "name": "Hook", "script": "..." },
3444
+ { "name": "Demo", "script": "..." },
3445
+ { "name": "CTA", "script": "..." }
3469
3446
  ],
3470
- "totalDurationInFrames": 450,
3471
- "fps": 30
3447
+ "voice": "Kore",
3448
+ "musicPrompt": "upbeat corporate"
3472
3449
  }
3473
- \`\`\`
3450
+ EOF
3451
+ \`\`\``
3452
+ };
3474
3453
 
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
3454
+ // src/commands/skill/generate-content.ts
3455
+ var DEFAULT_SECTIONS = [
3456
+ frontmatter,
3457
+ header3,
3458
+ prerequisites,
3459
+ workflow,
3460
+ createCommand2,
3461
+ createOptions,
3462
+ otherCommands,
3463
+ examples,
3464
+ output,
3465
+ videoCreation,
3466
+ bestPractices,
3467
+ fileTypes,
3468
+ troubleshooting
3469
+ ];
3470
+ function createSkillContext(brand2) {
3471
+ return {
3472
+ cmd: brand2.name,
3473
+ pkg: brand2.packageName,
3474
+ url: brand2.apiUrl,
3475
+ name: brand2.displayName
3476
+ };
3477
+ }
3478
+ function generateSkillContent(brand2, sections = DEFAULT_SECTIONS) {
3479
+ const ctx = createSkillContext(brand2);
3480
+ return sections.map((section) => section.render(ctx)).join("\n\n");
3481
+ }
3479
3482
 
3480
- ### Step 4: Create Scenes (Match Section Timing)
3483
+ // src/commands/skill/installer.ts
3484
+ import { mkdirSync, writeFileSync, existsSync as existsSync2, rmSync } from "fs";
3485
+ import { join, resolve as resolve4, relative } from "path";
3486
+ import { homedir } from "os";
3487
+
3488
+ // src/commands/skill/editors.ts
3489
+ var SUPPORTED_EDITORS = [
3490
+ { name: "Claude Code", dir: ".claude" },
3491
+ { name: "Cursor", dir: ".cursor" },
3492
+ { name: "Codex", dir: ".codex" },
3493
+ { name: "OpenCode", dir: ".opencode" },
3494
+ { name: "Windsurf", dir: ".windsurf" },
3495
+ { name: "Agent", dir: ".agent" }
3496
+ ];
3481
3497
 
3482
- **CRITICAL:** Use \`durationInFrames\` from each section. This ensures audio/video sync.
3498
+ // src/commands/skill/rules/video/content.ts
3499
+ var VIDEO_RULE_CONTENTS = [
3500
+ {
3501
+ filename: "video-creation-guide.md",
3502
+ content: `# Video Creation Guide
3483
3503
 
3484
- \`\`\`tsx
3485
- // Read manifest sections and create matching scenes
3486
- import manifest from '../../public/video-manifest.json';
3504
+ > **ALSO READ:**
3505
+ > - [project-based.md](project-based.md) - How to extract and animate real project components pixel-perfect
3506
+ > - If installed, check \`remotion-best-practices\` skill for Remotion-specific patterns
3487
3507
 
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
3508
+ ---
3492
3509
 
3493
- export const FULL_VIDEO_DURATION = manifest.totalDurationInFrames; // 450
3494
- \`\`\`
3510
+ # The "Kinetic SaaS" Motion Design System
3495
3511
 
3496
- Example scene component:
3512
+ **Objective:** Replicate the high-energy, fluid feel of premium tech product videos (Stripe, Apple, Linear, Affable.ai).
3497
3513
 
3498
- \`\`\`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';
3514
+ **Core Philosophy:** "Nothing sits still. Everything is physics-based. Every pixel breathes."
3502
3515
 
3503
- const section = manifest.sections[0];
3504
- export const SCENE_1_DURATION = section.durationInFrames;
3516
+ This is the difference between **Static Composition** (slideshows) and **Kinetic Composition** (motion graphics). You must rebuild UI as individual animated layers and film with a virtual camera.
3505
3517
 
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 } });
3518
+ ---
3510
3519
 
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
- };
3530
- \`\`\`
3520
+ ## 1. The Global Camera Rig (The "Anti-Static" Layer)
3531
3521
 
3532
- ### Step 5: Update FullVideo.tsx
3522
+ Even when reading text, the screen is slowly zooming or panning.
3533
3523
 
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
3524
+ ### Virtual Camera
3540
3525
 
3541
- export const FULL_VIDEO_DURATION = manifest.totalDurationInFrames;
3526
+ Every scene must be wrapped in a \`CameraRig\` component:
3542
3527
 
3543
- const BackgroundMusic: React.FC = () => {
3528
+ \`\`\`tsx
3529
+ const CameraRig: React.FC<{ children: React.ReactNode }> = ({ children }) => {
3544
3530
  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
- };
3531
+ const { durationInFrames } = useVideoConfig();
3532
+
3533
+ // The "Drift" - constant subtle movement
3534
+ const scale = interpolate(frame, [0, durationInFrames], [1.0, 1.05]);
3535
+ const rotation = interpolate(frame, [0, durationInFrames], [0, 0.5]);
3549
3536
 
3550
- export const FullVideo: React.FC = () => {
3551
3537
  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 />
3538
+ <AbsoluteFill style={{
3539
+ transform: \`scale(\${scale}) rotate(\${rotation}deg)\`,
3540
+ }}>
3541
+ {children}
3565
3542
  </AbsoluteFill>
3566
3543
  );
3567
3544
  };
3568
3545
  \`\`\`
3569
3546
 
3570
- ### Step 6: Preview & Render
3571
-
3572
- \`\`\`bash
3573
- npm run dev # Preview in Remotion Studio
3574
- npm run render # Output to out/video.mp4
3575
- \`\`\`
3576
-
3577
- ## CLI Commands Reference
3578
-
3579
- ### ${cmd2} video create
3580
-
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 |
3547
+ ### Zoom-Through Transitions
3590
3548
 
3591
- ### ${cmd2} tts generate
3592
-
3593
- \`\`\`bash
3594
- ${cmd2} tts generate -t "Narration text" -o voice.wav --voice Kore
3595
- ${cmd2} tts voices # List all voices
3596
- \`\`\`
3549
+ Transitions are NOT just fades - they are camera movements:
3597
3550
 
3598
- **Voices:** Kore (professional female), Puck (energetic male), Rachel (calm female), alloy (neutral)
3599
-
3600
- ### ${cmd2} music generate
3601
-
3602
- \`\`\`bash
3603
- ${cmd2} music generate -p "uplifting corporate" -d 30 -o music.mp3
3551
+ \`\`\`tsx
3552
+ // Camera scales rapidly into an element (logo, dot) until it fills screen
3553
+ const zoomThrough = interpolate(frame, [TRANSITION_START, TRANSITION_END], [1, 50]);
3554
+ // The element becomes the background for the next scene
3604
3555
  \`\`\`
3605
3556
 
3606
- **Good prompts:** "uplifting corporate", "calm ambient, soft piano", "cinematic orchestral"
3557
+ ---
3607
3558
 
3608
- ### ${cmd2} image search / video search
3559
+ ## 2. UI & Mockup Animation Rules (Rebuilding Reality)
3609
3560
 
3610
- \`\`\`bash
3611
- ${cmd2} image search -q "tropical beach" -n 5 -s large
3612
- ${cmd2} video search "tech workspace" -n 5
3613
- \`\`\`
3561
+ UI doesn't just fade in. It pops up with weight.
3614
3562
 
3615
- ### ${cmd2} mix create
3563
+ ### Priority: Use Real Project Components
3616
3564
 
3617
- Post-process audio into existing video:
3565
+ **If you're in a project folder, ALWAYS check for actual components first:**
3618
3566
 
3619
3567
  \`\`\`bash
3620
- ${cmd2} mix create --video video.mp4 --voice voice.wav --music music.mp3 -o final.mp4
3568
+ # Before building ANY UI, explore the project
3569
+ ls src/components/
3570
+ ls src/app/
3621
3571
  \`\`\`
3622
3572
 
3623
- ## Audio Guidelines
3573
+ **When project components exist:**
3574
+ 1. **Copy the actual component** into your Remotion project
3575
+ 2. **Strip logic** (remove useState, API calls, event handlers)
3576
+ 3. **Keep visuals identical** (same styles, colors, spacing)
3577
+ 4. **Add animation props** (progress, isHovered, etc.)
3578
+ 5. **Apply kinetic animation** using the rules below
3624
3579
 
3625
- | Element | Volume | Notes |
3626
- |---------|--------|-------|
3627
- | Voiceover | 100% | Primary audio |
3628
- | Background music | 20-30% | Fade in/out over ~10-20 frames |
3580
+ See [project-based.md](project-based.md) for the full component extraction process.
3629
3581
 
3630
- Generate music 5s longer than video for fade out.
3582
+ **This is the priority order:**
3583
+ 1. \u2705 Pixel-perfect copy of real project components (animated)
3584
+ 2. \u26A0\uFE0F Recreate UI from project's design system (colors, fonts, spacing)
3585
+ 3. \u274C Generic mockups or stock images (ONLY for non-UI scenes like hooks)
3631
3586
 
3632
- ## Animation Quality Checklist
3587
+ ### The "Pop-Up" Entrance (2.5D Rotation)
3633
3588
 
3634
- Before rendering, ensure your video follows these standards from motion-standards.md:
3589
+ \`\`\`tsx
3590
+ // Heavy spring for weighty feel
3591
+ const progress = spring({
3592
+ frame,
3593
+ fps,
3594
+ config: { mass: 2, damping: 20, stiffness: 100 },
3595
+ });
3635
3596
 
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
3597
+ // Start tilted back, spring to flat
3598
+ const rotateX = interpolate(progress, [0, 1], [20, 0]);
3599
+ const y = interpolate(progress, [0, 1], [100, 0]);
3600
+ const scale = interpolate(progress, [0, 1], [0.8, 1.0]);
3640
3601
 
3641
- ---
3642
-
3643
- # \u{1F3AC} PROJECT-BASED VIDEO WORKFLOW (Mode A)
3602
+ <div style={{
3603
+ transform: \`perspective(1000px) rotateX(\${rotateX}deg) translateY(\${y}px) scale(\${scale})\`,
3604
+ }}>
3605
+ {uiComponent}
3606
+ </div>
3607
+ \`\`\`
3644
3608
 
3645
- **Use this when user has a project/app to showcase.**
3609
+ ### Cursor Simulation
3646
3610
 
3647
- ## \u{1F4CB} PHASE 1: EXPLORE THE APP
3611
+ **Movement:** Never linear. Cursors move in **Bezier Curves** with an arc.
3648
3612
 
3649
- ### 1.1 Find Brand Assets
3613
+ \`\`\`tsx
3614
+ // 1. Spring progress from start to end
3615
+ const progress = spring({
3616
+ frame: frame - startFrame,
3617
+ fps,
3618
+ config: { damping: 20, stiffness: 80 }, // Slower, deliberate
3619
+ });
3650
3620
 
3651
- \`\`\`bash
3652
- # Logo
3653
- find src -name "*[Ll]ogo*" 2>/dev/null
3654
- find public -name "*logo*" 2>/dev/null
3621
+ // 2. Linear interpolation for X and Y
3622
+ const linearX = interpolate(progress, [0, 1], [start.x, end.x]);
3623
+ const linearY = interpolate(progress, [0, 1], [start.y, end.y]);
3655
3624
 
3656
- # Colors - THIS IS CRITICAL
3657
- cat tailwind.config.* | grep -A 30 "colors"
3658
- cat src/app/globals.css | head -50
3625
+ // 3. THE ARC: Parabola that peaks mid-travel (this is the secret sauce)
3626
+ const arcHeight = 100; // How much the cursor "loops"
3627
+ const arcOffset = Math.sin(progress * Math.PI) * arcHeight;
3659
3628
 
3660
- # Fonts
3661
- grep -r "fontFamily" tailwind.config.* src/app/layout.tsx
3629
+ // 4. Apply arc to Y position
3630
+ const cursorY = linearY - arcOffset;
3662
3631
  \`\`\`
3663
3632
 
3664
- ### 1.2 Read Key UI Components
3633
+ **Click Interaction:**
3634
+ \`\`\`tsx
3635
+ // On click:
3636
+ // 1. Cursor scales down
3637
+ const cursorScale = isClicking ? 0.8 : 1;
3665
3638
 
3666
- **Don't copy - just read to understand the visual structure:**
3639
+ // 2. Button squishes
3640
+ const buttonScaleX = isClicking ? 1.05 : 1;
3641
+ const buttonScaleY = isClicking ? 0.95 : 1;
3667
3642
 
3668
- \`\`\`bash
3669
- # Find main components
3670
- find src/components -name "*.tsx" | head -30
3643
+ // 3. Release both with spring
3644
+ \`\`\`
3671
3645
 
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
3646
+ **Standard Cursor SVG:**
3647
+ \`\`\`tsx
3648
+ // Mac-style cursor (use as reference, customize as needed)
3649
+ <svg width="32" height="32" viewBox="0 0 32 32" fill="none">
3650
+ <path
3651
+ d="M4 4L11.5 26L16 17.5L25 25L27 23L18.5 15L28 11.5L4 4Z"
3652
+ fill="black"
3653
+ stroke="white"
3654
+ strokeWidth="2"
3655
+ />
3656
+ </svg>
3676
3657
  \`\`\`
3677
3658
 
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)
3659
+ ### Staggered Lists & Grids
3683
3660
 
3684
- ### 1.3 Document Your Findings
3661
+ **Rule:** NEVER show a list or grid all at once.
3685
3662
 
3686
- \`\`\`markdown
3687
- ## Brand Discovery: [App Name]
3663
+ \`\`\`tsx
3664
+ const STAGGER_FRAMES = 4; // ~0.05s at 60fps
3688
3665
 
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)
3666
+ {items.map((item, i) => {
3667
+ const delay = i * STAGGER_FRAMES;
3668
+ const progress = spring({
3669
+ frame: frame - delay,
3670
+ fps,
3671
+ config: { damping: 15, stiffness: 120 },
3672
+ });
3695
3673
 
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
3674
+ return (
3675
+ <div style={{
3676
+ opacity: progress,
3677
+ transform: \`translateY(\${interpolate(progress, [0, 1], [20, 0])}px)\`,
3678
+ }}>
3679
+ {item}
3680
+ </div>
3681
+ );
3682
+ })}
3700
3683
  \`\`\`
3701
3684
 
3702
3685
  ---
3703
3686
 
3704
- ## \u{1F4CB} PHASE 2: PLAN THE VIDEO
3705
-
3706
- ### Scene Structure
3687
+ ## 3. Kinetic Typography (Text that Hits)
3707
3688
 
3708
- \`\`\`markdown
3709
- ## Video Plan: [App Name] Demo
3689
+ Text doesn't fade. It slams in, changes fill, or slides up.
3710
3690
 
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
3691
+ ### Pattern A: The "Masked Reveal"
3715
3692
 
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
3693
+ Text rises from a floor:
3721
3694
 
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
3726
-
3727
- ### Scene 4: CTA (3s / 90 frames)
3728
- **What to show:** Logo + CTA button + URL
3729
- **Animation:** Logo fades in, button pulses
3695
+ \`\`\`tsx
3696
+ <div style={{ overflow: 'hidden', height: 80 }}>
3697
+ <h1 style={{
3698
+ transform: \`translateY(\${interpolate(progress, [0, 1], [100, 0])}%)\`,
3699
+ }}>
3700
+ INTRODUCING
3701
+ </h1>
3702
+ </div>
3730
3703
  \`\`\`
3731
3704
 
3732
- ---
3705
+ ### Pattern B: Variable Weight (Animate Keywords)
3733
3706
 
3734
- ## \u{1F528} PHASE 3: BUILD
3707
+ Don't animate the whole sentence. Animate **keywords**:
3735
3708
 
3736
- ### 3.1 Create Fresh Project
3737
-
3738
- \`\`\`bash
3739
- rm -rf ../appname-video
3740
- ${cmd2} video init ../appname-video
3741
- cd ../appname-video
3709
+ \`\`\`tsx
3710
+ // "Teams waste HOURS" - only "HOURS" animates
3711
+ <p>
3712
+ Teams waste{' '}
3713
+ <span style={{
3714
+ transform: \`scale(\${interpolate(keywordProgress, [0, 1], [1, 1.2])})\`,
3715
+ color: interpolateColors(keywordProgress, [0, 1], ['#F0F0F0', '#FF6B6B']),
3716
+ textShadow: \`0 0 \${glowProgress * 20}px rgba(255,107,107,0.5)\`,
3717
+ }}>
3718
+ HOURS
3719
+ </span>
3720
+ </p>
3742
3721
  \`\`\`
3743
3722
 
3744
- ### 3.2 Copy Brand Assets Only
3723
+ ### Pattern C: The "Glitch/Tech" Accent
3745
3724
 
3746
- \`\`\`bash
3747
- # Logo
3748
- cp ../myapp/public/logo.svg ./public/
3749
-
3750
- # Tailwind config (for colors/fonts)
3751
- cp ../myapp/tailwind.config.* ./
3725
+ Chromatic aberration on impact:
3752
3726
 
3753
- # Global CSS
3754
- cp ../myapp/src/app/globals.css ./src/styles/
3727
+ \`\`\`tsx
3728
+ // Duplicate text, offset by 2px with color channels, flash for 2 frames
3729
+ {isImpactFrame && (
3730
+ <>
3731
+ <span style={{ position: 'absolute', left: -2, color: '#FF0000', opacity: 0.5 }}>TEXT</span>
3732
+ <span style={{ position: 'absolute', left: 2, color: '#0000FF', opacity: 0.5 }}>TEXT</span>
3733
+ </>
3734
+ )}
3755
3735
  \`\`\`
3756
3736
 
3757
- ### 3.3 Build Scene Components - PIXEL PERFECT
3737
+ ### Text Colors
3758
3738
 
3759
- **Each scene replicates what you observed, using Remotion:**
3739
+ Never pure white (\`#FFF\`). Use \`#F0F0F0\` with subtle gradient or shadow for depth.
3760
3740
 
3761
- \`\`\`tsx
3762
- // src/remotion/scenes/SidebarScene.tsx
3763
- // Replicates: src/components/slides/SlidesSidebar.tsx
3741
+ ---
3764
3742
 
3765
- import React from "react";
3766
- import { AbsoluteFill, useCurrentFrame, spring, useVideoConfig } from "remotion";
3743
+ ## 4. Atmosphere & Backgrounds (The "Deep Space")
3767
3744
 
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
- ];
3745
+ Background is never a solid color. It's deep black with floating colored orbs.
3773
3746
 
3774
- export const SIDEBAR_SCENE_DURATION = 150;
3747
+ ### The "Orb" System
3775
3748
 
3776
- export const SidebarScene: React.FC = () => {
3749
+ \`\`\`tsx
3750
+ const MovingBackground: React.FC = () => {
3777
3751
  const frame = useCurrentFrame();
3778
- const { fps } = useVideoConfig();
3779
3752
 
3780
- const sidebarProgress = spring({ frame, fps, config: { damping: 20, stiffness: 100 } });
3781
- const sidebarX = (1 - sidebarProgress) * -280;
3753
+ // Orbs move in figure-8 or circular patterns
3754
+ const orb1X = Math.sin(frame / 60) * 200;
3755
+ const orb1Y = Math.cos(frame / 80) * 100;
3756
+ const orb2X = Math.sin(frame / 70 + Math.PI) * 150;
3757
+ const orb2Y = Math.cos(frame / 90 + Math.PI) * 120;
3782
3758
 
3783
3759
  return (
3784
- <AbsoluteFill style={{ backgroundColor: "#0f172a" }}>
3785
- {/* Sidebar - EXACT colors from tailwind.config */}
3760
+ <AbsoluteFill style={{ backgroundColor: '#0a0a0a' }}>
3761
+ {/* Orb 1 - Teal */}
3786
3762
  <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>
3763
+ position: 'absolute',
3764
+ width: 600,
3765
+ height: 600,
3766
+ borderRadius: '50%',
3767
+ background: 'radial-gradient(circle, rgba(20,184,166,0.3) 0%, transparent 70%)',
3768
+ filter: 'blur(100px)',
3769
+ transform: \`translate(\${orb1X}px, \${orb1Y}px)\`,
3770
+ left: '20%',
3771
+ top: '30%',
3772
+ }} />
3773
+
3774
+ {/* Orb 2 - Purple */}
3775
+ <div style={{
3776
+ position: 'absolute',
3777
+ width: 500,
3778
+ height: 500,
3779
+ borderRadius: '50%',
3780
+ background: 'radial-gradient(circle, rgba(168,85,247,0.3) 0%, transparent 70%)',
3781
+ filter: 'blur(100px)',
3782
+ transform: \`translate(\${orb2X}px, \${orb2Y}px)\`,
3783
+ right: '20%',
3784
+ bottom: '20%',
3785
+ }} />
3833
3786
  </AbsoluteFill>
3834
3787
  );
3835
3788
  };
3836
3789
  \`\`\`
3837
3790
 
3838
- ### 3.4 Key Principles: PIXEL-PERFECT Replication
3839
-
3840
- **The video UI should be indistinguishable from the real app.**
3791
+ ### Vignette & Noise
3841
3792
 
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
3793
+ \`\`\`tsx
3794
+ // Noise texture overlay (5% opacity)
3795
+ <AbsoluteFill style={{
3796
+ backgroundImage: 'url(/noise.png)',
3797
+ opacity: 0.05,
3798
+ mixBlendMode: 'overlay',
3799
+ }} />
3800
+
3801
+ // Vignette (dark corners)
3802
+ <AbsoluteFill style={{
3803
+ background: 'radial-gradient(ellipse at center, transparent 40%, rgba(0,0,0,0.8) 100%)',
3804
+ }} />
3805
+ \`\`\`
3848
3806
 
3849
3807
  ---
3850
3808
 
3851
- ## \u{1F3AC} PHASE 4: AUDIO & RENDER
3809
+ ## 5. Physics & Timing Reference
3852
3810
 
3853
- ### Generate Audio
3811
+ ### Spring Configs
3854
3812
 
3855
- \`\`\`bash
3856
- ${cmd2} video create \\
3857
- --script "Your narration..." \\
3858
- --music-prompt "modern uplifting tech" \\
3859
- --output ./public
3860
- \`\`\`
3813
+ | Use Case | Config | Feel |
3814
+ |----------|--------|------|
3815
+ | Standard (snappy) | \`{ mass: 1, damping: 15, stiffness: 120 }\` | Smooth, professional |
3816
+ | Heavy (UI mockups) | \`{ mass: 2, damping: 20, stiffness: 100 }\` | Weighty, premium |
3817
+ | Bouncy (attention) | \`{ mass: 1, damping: 10, stiffness: 150 }\` | Playful overshoot |
3861
3818
 
3862
- ### Preview & Render
3819
+ ### Timing Values
3863
3820
 
3864
- \`\`\`bash
3865
- npm run dev # Preview
3866
- npm run render # Output to out/video.mp4
3867
- \`\`\`
3821
+ | Action | Frames | Notes |
3822
+ |--------|--------|-------|
3823
+ | Element entrance | 15-20 | Spring to rest |
3824
+ | Stagger gap | 3-5 | Between list items |
3825
+ | Hold on key info | 45-60 | Minimum read time |
3826
+ | Scene transition | 20-30 | Zoom-through |
3827
+
3828
+ ### The Golden Rule
3829
+
3830
+ Use \`interpolate(frame)\` to ensure \`scale\` or \`rotation\` is changing on **EVERY frame**. No static resting states.
3868
3831
 
3869
3832
  ---
3870
3833
 
3871
- ## \u274C WHAT NOT TO DO
3834
+ ## 6. Micro-Tricks from Premium Videos
3872
3835
 
3873
- ### Bad: Generic rectangles
3874
- \`\`\`tsx
3875
- // \u274C NO
3876
- <div style={{ background: "linear-gradient(#667eea, #764ba2)", width: 200, height: 150 }} />
3877
- \`\`\`
3836
+ ### The "Match Cut"
3878
3837
 
3879
- ### Bad: Made-up content
3880
- \`\`\`tsx
3881
- // \u274C NO
3882
- <h2>Key Insights from Research</h2>
3883
- <li>Finding 1: Performance improved by 45%</li>
3884
- \`\`\`
3838
+ Small circle zooms to fill screen, becomes next scene's background:
3885
3839
 
3886
- ### Bad: Not matching the app
3887
3840
  \`\`\`tsx
3888
- // \u274C NO - App uses slate-900, not gray-800
3889
- <div style={{ backgroundColor: "#1f2937" }}>
3841
+ // Circle element scales 50x to wipe to next scene
3842
+ const circleScale = interpolate(frame, [MATCH_START, MATCH_END], [1, 50]);
3890
3843
  \`\`\`
3891
3844
 
3892
- ### Good: Replicated UI with correct brand
3845
+ ### Search Bar Typing
3846
+
3847
+ Include blinking cursor:
3848
+
3893
3849
  \`\`\`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>
3850
+ const Typewriter: React.FC<{ text: string }> = ({ text }) => {
3851
+ const frame = useCurrentFrame();
3852
+ const charIndex = Math.floor(frame / 3); // 3 frames per char
3853
+ const showCursor = Math.floor(frame / 15) % 2 === 0; // Blink every 15 frames
3854
+
3855
+ return (
3856
+ <span>
3857
+ {text.slice(0, charIndex)}
3858
+ {showCursor && <span style={{ opacity: 0.8 }}>|</span>}
3859
+ </span>
3860
+ );
3861
+ };
3898
3862
  \`\`\`
3899
3863
 
3900
- ---
3864
+ ### The "Card Fan"
3901
3865
 
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
- ---
3866
+ Stack cards, then fan out:
3929
3867
 
3930
- # Motion Design Standards
3868
+ \`\`\`tsx
3869
+ const cards = [
3870
+ { rotation: -5, x: -20 },
3871
+ { rotation: 0, x: 0 },
3872
+ { rotation: 5, x: 20 },
3873
+ ];
3931
3874
 
3932
- Generate videos that feel like high-end productions (Apple, Stripe, Linear quality).
3875
+ {cards.map((card, i) => {
3876
+ const fanProgress = spring({ frame: frame - 30, fps, config: { damping: 15, stiffness: 100 } });
3933
3877
 
3934
- **Follow these standards for every Remotion component.**
3878
+ return (
3879
+ <div style={{
3880
+ position: 'absolute',
3881
+ transform: \`rotate(\${fanProgress * card.rotation}deg) translateX(\${fanProgress * card.x}px)\`,
3882
+ zIndex: i,
3883
+ }}>
3884
+ <ProfileCard />
3885
+ </div>
3886
+ );
3887
+ })}
3888
+ \`\`\`
3935
3889
 
3936
- ## STANDARD 01: PHYSICS OVER LINEARITY
3890
+ ---
3937
3891
 
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 }\`
3892
+ ## 7. Strict Animation Guidelines (DO NOT DEVIATE)
3941
3893
 
3942
- \`\`\`tsx
3943
- // BAD
3944
- const opacity = interpolate(frame, [0, 30], [0, 1]);
3894
+ **ROLE:** Senior Motion Graphics Engineer for Remotion.
3895
+ **REFERENCE STYLE:** "High-End SaaS Product Launch" (e.g., Affable.ai, Linear.app).
3945
3896
 
3946
- // GOOD
3947
- const progress = spring({ frame, fps, config: { mass: 0.8, stiffness: 150, damping: 15 } });
3948
- \`\`\`
3897
+ ### Physics & Timing
3949
3898
 
3950
- ## STANDARD 02: ORCHESTRATION & CASCADE
3899
+ - **No Linear Motion:** All spatial movement (X, Y, Scale) must use \`spring\`.
3900
+ - *Standard Spring:* \`{ mass: 1, damping: 15, stiffness: 120 }\` (Snappy but smooth)
3901
+ - *Heavy Spring (Mockups):* \`{ mass: 2, damping: 20, stiffness: 100 }\` (Feels weighty)
3902
+ - **Continuous Flow:** Use \`interpolate(frame)\` to ensure \`scale\` or \`rotation\` is changing slightly on EVERY frame. No static resting states.
3951
3903
 
3952
- - **Rule:** NEVER animate all elements simultaneously
3953
- - **Implementation:** Staggered entrances with 3-5 frames between items
3904
+ ### UI Component Behavior
3954
3905
 
3955
- \`\`\`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
- ))}
3967
- \`\`\`
3906
+ - **Entrance:** When a UI card enters, it must use **2.5D Rotation**.
3907
+ - *Code:* \`transform: perspective(1000px) rotateX(\${rotateX}deg) translateY(\${y}px)\`
3908
+ - *Values:* Start at \`rotateX(20deg)\` and \`y(100px)\`. Spring to \`0\`.
3909
+ - **Internal Staggering:** If the UI card contains a list or grid, use \`<Sequence>\` or \`delay\` to reveal items one by one (3 frame gap).
3910
+ - **Cursor Interaction:** If a cursor clicks a button:
3911
+ 1. Cursor scales \`1 -> 0.8\`
3912
+ 2. Button scales \`1 -> 0.95\`
3913
+ 3. Release both to \`1\` with a spring
3968
3914
 
3969
- ## STANDARD 03: THE VIRTUAL CAMERA
3915
+ ### Typography Patterns
3970
3916
 
3971
- - **Rule:** Even when UI is idle, add subtle movement
3972
- - **Implementation:** Dolly zoom (slow push in)
3917
+ - **Pattern A (Headline):** "Masked Slide Up". Text translates Y from \`100%\` inside a clipped container.
3918
+ - **Pattern B (Keywords):** "Scale & Glow". Keywords scale to \`1.1\` and emit a \`text-shadow\`.
3919
+ - **Colors:** Text is never pure white (\`#FFF\`). Use \`#F0F0F0\` with a subtle gradient or shadow to add depth.
3973
3920
 
3974
- \`\`\`tsx
3975
- const CinematicContainer = ({ children }) => {
3976
- const frame = useCurrentFrame();
3977
- const { durationInFrames } = useVideoConfig();
3978
- const scale = interpolate(frame, [0, durationInFrames], [1, 1.03]);
3921
+ ### Background "Aliveness"
3979
3922
 
3980
- return (
3981
- <AbsoluteFill style={{ transform: \`scale(\${scale})\` }}>
3982
- {children}
3983
- </AbsoluteFill>
3984
- );
3985
- };
3986
- \`\`\`
3923
+ - Create a \`<MovingBackground>\` component.
3924
+ - It must feature 2-3 \`AbsoluteFill\` gradient orbs that move in a continuous loop using \`Math.sin(frame / slowFactor)\`.
3925
+ - This ensures the video has "depth" behind the text.
3987
3926
 
3988
- ## STANDARD 04: HUMAN SIMULATION
3927
+ ---
3989
3928
 
3990
- - **Rule:** NEVER move cursor in straight lines
3991
- - **Implementation:** Use curved/Bezier paths for cursor movement
3929
+ ## 8. Seamless Scene Transitions (No Hard Cuts)
3992
3930
 
3993
- ## STANDARD 05: TECHNICAL CONSTRAINTS
3931
+ In high-end motion design, transitions are rarely separate "effects" (like a cross-dissolve). The *outgoing* scene physically pushes, slides, or zooms into the *incoming* scene.
3994
3932
 
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()\`
3933
+ ### The "Wipe" Rule (No Hard Cuts)
3998
3934
 
3999
- ## Execution Checklist
3935
+ - **Forbidden:** Never let Scene A end at frame X and Scene B start at frame X+1 with a simple cut.
3936
+ - **Required:** Scene B must exist *above* Scene A (z-index) for at least 15-20 frames before the cut happens.
3937
+ - **Mechanism:** Scene B enters using a "Wipe" motion (sliding in from the bottom/right) or a "Scale In" (expanding from a dot).
4000
3938
 
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() {
4009
- return `---
4010
- name: micro-interactions
4011
- description: Animation components and patterns
4012
- ---
3939
+ ### The "Zoom-Through" Technique (Best for SaaS)
3940
+
3941
+ As Scene A ends, the camera must **accelerate** its zoom:
4013
3942
 
4014
- # Micro-Interactions
3943
+ \`\`\`tsx
3944
+ // Scene A Action: At duration - 20 frames, zoom way in
3945
+ const scaleA = interpolate(frame, [duration - 20, duration], [1.0, 5.0]);
3946
+
3947
+ // Scene B Action: Starts at scale 0.5 and springs to 1.0
3948
+ const scaleB = spring({ frame, fps, config: { damping: 14, stiffness: 120 } });
3949
+ const scaleBValue = interpolate(scaleB, [0, 1], [0.5, 1]);
3950
+ \`\`\`
4015
3951
 
4016
- ## Core Principles
3952
+ Result: It looks like the camera flew *through* Scene A to find Scene B behind it.
4017
3953
 
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
3954
+ ### The "Color Swipe" (Safety Net)
4022
3955
 
4023
- ## Spring Configurations
3956
+ If a complex match-cut is too hard, use a "Curtain":
4024
3957
 
4025
3958
  \`\`\`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
- };
3959
+ // A solid colored AbsoluteFill slides in from left, covers screen for 2 frames,
3960
+ // then slides out to the right, revealing Scene B
3961
+ const curtainX = interpolate(frame, [0, 10, 12, 22], [-100, 0, 0, 100]);
4032
3962
  \`\`\`
4033
3963
 
4034
- ## Entry Animations
3964
+ ### TransitionWrapper Component
4035
3965
 
4036
- ### Fade + Slide
3966
+ Use the \`TransitionWrapper\` from shared components:
4037
3967
 
4038
3968
  \`\`\`tsx
4039
- const AnimatedEntry = ({ delay = 0, direction = 'up', children }) => {
4040
- const frame = useCurrentFrame();
4041
- const { fps } = useVideoConfig();
3969
+ import { TransitionWrapper } from './shared';
4042
3970
 
4043
- const progress = spring({
4044
- frame: frame - delay,
4045
- fps,
4046
- config: { damping: 20, stiffness: 100 }
4047
- });
3971
+ // Types: 'slide' | 'zoom' | 'fade' | 'none'
3972
+ // enterFrom: 'left' | 'right' | 'bottom'
4048
3973
 
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
- };
3974
+ <TransitionWrapper type="slide" enterFrom="bottom">
3975
+ <YourScene />
3976
+ </TransitionWrapper>
3977
+ \`\`\`
3978
+
3979
+ ### Critical: Overlap Your Sequences
4055
3980
 
4056
- const { x, y } = directions[direction];
3981
+ **This is the most important part.** To make transitions work, you must overlap sequences slightly. If Scene 1 is 90 frames, start Scene 2 at frame 75 (15-frame overlap).
4057
3982
 
3983
+ \`\`\`tsx
3984
+ export const MyVideo = () => {
4058
3985
  return (
4059
- <div style={{
4060
- opacity: progress,
4061
- transform: \`translate(\${x * (1 - progress)}px, \${y * (1 - progress)}px)\`,
4062
- }}>
4063
- {children}
4064
- </div>
3986
+ <AbsoluteFill>
3987
+ {/* SCENE 1: Ends at frame 100 */}
3988
+ <Sequence from={0} durationInFrames={100}>
3989
+ <SceneOne />
3990
+ </Sequence>
3991
+
3992
+ {/* SCENE 2: Starts at frame 85 (15 frames early!) */}
3993
+ {/* TransitionWrapper slides it ON TOP of Scene 1 */}
3994
+ <Sequence from={85} durationInFrames={150}>
3995
+ <TransitionWrapper type="slide" enterFrom="bottom">
3996
+ <SceneTwo />
3997
+ </TransitionWrapper>
3998
+ </Sequence>
3999
+
4000
+ {/* SCENE 3: Starts early again */}
4001
+ <Sequence from={220} durationInFrames={150}>
4002
+ <TransitionWrapper type="zoom">
4003
+ <SceneThree />
4004
+ </TransitionWrapper>
4005
+ </Sequence>
4006
+ </AbsoluteFill>
4065
4007
  );
4066
4008
  };
4067
4009
  \`\`\`
4068
4010
 
4069
- ### Staggered List
4011
+ **Why this fixes the "Light Switch" effect:** Because Scene 2 physically slides *over* Scene 1 while Scene 1 is still visible underneath, your brain registers it as continuous movement rather than a "cut."
4070
4012
 
4071
- \`\`\`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
- );
4079
- \`\`\`
4080
-
4081
- ## Interaction Simulation
4013
+ ---
4082
4014
 
4083
- ### Button Press
4015
+ ## Quick Tips from Motion Devs
4084
4016
 
4085
- \`\`\`tsx
4086
- const ButtonPress = ({ pressFrame, children }) => {
4087
- const frame = useCurrentFrame();
4088
- const { fps } = useVideoConfig();
4017
+ - **Don't use white backgrounds.** The 2.5D lighting and shadows pop much better on dark grey or black backgrounds.
4018
+ - **Overlap delays.** If Title starts at frame 10 and Card starts at frame 30, the title hasn't finished moving when the card appears. This overlap is crucial for fluidity.
4019
+ - **High FPS.** Ensure your \`remotion.config.ts\` is set to 60fps for springs to look smooth.
4089
4020
 
4090
- const isPressing = frame >= pressFrame && frame < pressFrame + 3;
4091
- const isReleasing = frame >= pressFrame + 3;
4021
+ ---
4092
4022
 
4093
- const releaseProgress = isReleasing ? spring({
4094
- frame: frame - pressFrame - 3,
4095
- fps,
4096
- config: { damping: 10, stiffness: 300 }
4097
- }) : 0;
4023
+ ## Self-Check: Is It Kinetic?
4098
4024
 
4099
- const scale = isPressing ? 0.95 : (0.95 + releaseProgress * 0.05);
4025
+ Before rendering, verify:
4100
4026
 
4101
- return <div style={{ transform: \`scale(\${scale})\` }}>{children}</div>;
4102
- };
4103
- \`\`\`
4027
+ - [ ] Camera rig wraps entire scene with drift/zoom
4028
+ - [ ] Every UI element uses 2.5D rotation entrance
4029
+ - [ ] Cursor moves in curves with overshoot
4030
+ - [ ] Lists/grids stagger (never appear all at once)
4031
+ - [ ] Text uses masked reveal or keyword animation
4032
+ - [ ] Background has moving orbs + vignette + noise
4033
+ - [ ] Something is moving on EVERY frame
4034
+ - [ ] No static resting states longer than 30 frames
4035
+ - [ ] Scene transitions overlap (no hard cuts between scenes)
4036
+ - [ ] TransitionWrapper used for slide/zoom entrances
4104
4037
 
4105
- ### Typed Text
4038
+ If your video looks like PowerPoint slides with voiceover, **START OVER**.
4039
+ `
4040
+ },
4041
+ {
4042
+ filename: "failures.md",
4043
+ content: `# Common Failures - READ THIS FIRST
4106
4044
 
4107
- \`\`\`tsx
4108
- const TypedText = ({ text, startFrame = 0, speed = 2 }) => {
4109
- const frame = useCurrentFrame();
4110
- const charsToShow = Math.floor((frame - startFrame) / speed);
4045
+ If your video looks like any of these, START OVER.
4111
4046
 
4112
- if (frame < startFrame) return null;
4047
+ ## FAILURE 1: Slideshow
4113
4048
 
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
- };
4123
4049
  \`\`\`
4124
-
4125
- ### Counting Number
4126
-
4127
- \`\`\`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>;
4135
- };
4050
+ What you made:
4051
+ - Background image
4052
+ - Text appears on top
4053
+ - Text disappears
4054
+ - New background image
4055
+ - More text
4056
+
4057
+ This is PowerPoint, not a video. REJECTED.
4136
4058
  \`\`\`
4137
4059
 
4138
- ## Timing Guidelines
4060
+ ## FAILURE 2: Lorem Ipsum / Placeholder Content
4139
4061
 
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
- };
4062
+ \`\`\`
4063
+ What you made:
4064
+ - "Lorem ipsum dolor sit amet..."
4065
+ - "Sample text here"
4066
+ - Generic placeholder content
4164
4067
 
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();
4068
+ Use REAL content from the project. REJECTED.
4069
+ \`\`\`
4174
4070
 
4175
- const progress = spring({
4176
- frame: frame - delay,
4177
- fps,
4178
- config: SPRING_CONFIGS.smooth,
4179
- });
4071
+ ## FAILURE 3: Static UI Screenshot
4180
4072
 
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
- };
4073
+ \`\`\`
4074
+ What you made:
4075
+ - Screenshot of the app
4076
+ - Text overlay saying "Our Dashboard"
4077
+ - No motion, no interaction
4188
4078
 
4189
- const { x, y } = directions[direction];
4079
+ Recreate the UI in code and ANIMATE it. REJECTED.
4080
+ \`\`\`
4190
4081
 
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
- };
4082
+ ## FAILURE 4: Elements Just Appearing
4200
4083
 
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();
4084
+ \`\`\`
4085
+ What you made:
4086
+ - Frame 0: nothing
4087
+ - Frame 1: element is fully visible
4088
+ - No transition, no animation
4209
4089
 
4210
- const progress = spring({
4211
- frame: frame - delay,
4212
- fps,
4213
- config: SPRING_CONFIGS.bouncy,
4214
- });
4090
+ Every element must animate in with spring/easing. REJECTED.
4091
+ \`\`\`
4215
4092
 
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
- };
4093
+ ## FAILURE 5: Hard Cuts Between Scenes (The "Light Switch" Effect)
4225
4094
 
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
- );
4095
+ \`\`\`
4096
+ What you made:
4097
+ - Scene 1 ends at frame 100
4098
+ - Scene 2 starts at frame 101
4099
+ - No overlap, no slide, no zoom-through
4100
+ - Feels like a light switching on/off
4101
+
4102
+ FIX: Overlap sequences by 15-20 frames. Scene 2 must slide/zoom
4103
+ ON TOP of Scene 1 while Scene 1 is still visible. Use TransitionWrapper.
4104
+ REJECTED.
4105
+ \`\`\`
4238
4106
 
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();
4107
+ ## FAILURE 6: Static Camera
4246
4108
 
4247
- const isPressing = frame >= pressFrame && frame < pressFrame + 3;
4248
- const isReleasing = frame >= pressFrame + 3;
4109
+ \`\`\`
4110
+ What you made:
4111
+ - Camera never moves
4112
+ - No drift, no zoom, no rotation
4113
+ - Feels like watching a screenshot
4249
4114
 
4250
- const releaseProgress = isReleasing ? spring({
4251
- frame: frame - pressFrame - 3,
4252
- fps,
4253
- config: { damping: 10, stiffness: 300 },
4254
- }) : 0;
4115
+ Camera must ALWAYS be subtly moving. REJECTED.
4116
+ \`\`\`
4255
4117
 
4256
- const scale = isPressing ? 0.95 : 0.95 + releaseProgress * 0.05;
4118
+ ## FAILURE 7: Linear Cursor Movement
4257
4119
 
4258
- return <div style={{ transform: \`scale(\${Math.min(1, scale)})\` }}>{children}</div>;
4259
- };
4120
+ \`\`\`
4121
+ What you made:
4122
+ - Cursor moves in straight lines
4123
+ - No overshoot on stops
4124
+ - Click has no feedback
4260
4125
 
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);
4126
+ Cursor must move in Bezier curves with overshoot. REJECTED.
4127
+ \`\`\`
4128
+ `
4129
+ },
4130
+ {
4131
+ filename: "parameterization.md",
4132
+ content: `# Parameterization (Critical)
4270
4133
 
4271
- if (frame < startFrame) return null;
4134
+ Never hardcode frame numbers. Use variables for all keyframes.
4272
4135
 
4273
- const isTyping = charsToShow < text.length;
4136
+ ## Why
4274
4137
 
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
- };
4138
+ When you change timing early in video, everything else breaks if hardcoded.
4284
4139
 
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();
4140
+ ## Pattern
4294
4141
 
4295
- const progress = interpolate(frame - startFrame, [0, duration], [0, 1], {
4296
- extrapolateLeft: 'clamp',
4297
- extrapolateRight: 'clamp',
4298
- });
4142
+ \`\`\`ts
4143
+ // Define keyframes as variables
4144
+ const SCENE_START = 0;
4145
+ const TITLE_IN = SCENE_START + 15;
4146
+ const TITLE_HOLD = TITLE_IN + 60;
4147
+ const TITLE_OUT = TITLE_HOLD + 15;
4148
+ const SUBTITLE_IN = TITLE_IN + 20; // Relative to title
4149
+ const SCENE_END = TITLE_OUT + 30;
4299
4150
 
4300
- const eased = 1 - Math.pow(1 - progress, 3);
4301
- const value = Math.round(from + (to - from) * eased);
4151
+ // Use in animations
4152
+ opacity: interpolate(frame, [TITLE_IN, TITLE_IN + 15], [0, 1])
4153
+ \`\`\`
4302
4154
 
4303
- return <span>{format(value)}</span>;
4304
- };
4155
+ ## Scene Duration
4305
4156
 
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;
4157
+ \`\`\`ts
4158
+ // Calculate from keyframes, don't hardcode
4159
+ const sceneDuration = SCENE_END - SCENE_START;
4160
+ \`\`\`
4314
4161
 
4315
- return <div style={{ transform: \`translateY(\${y}px)\` }}>{children}</div>;
4316
- };
4162
+ ## Audio Sync
4317
4163
 
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();
4164
+ \`\`\`ts
4165
+ // If audio timestamp changes, only update one variable
4166
+ const VOICEOVER_START = 45;
4167
+ const VISUAL_CUE = VOICEOVER_START + 10; // Auto-adjusts
4168
+ \`\`\`
4326
4169
 
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
- });
4170
+ ## Multi-Scene
4333
4171
 
4334
- const scale = isActive ? 1 + progress * 0.03 : 1;
4172
+ \`\`\`ts
4173
+ const SCENE_1_START = 0;
4174
+ const SCENE_1_END = SCENE_1_START + 90;
4175
+ const SCENE_2_START = SCENE_1_END - 15; // 15 frame overlap for transition
4176
+ const SCENE_2_END = SCENE_2_START + 120;
4177
+ \`\`\`
4178
+ `
4179
+ },
4180
+ {
4181
+ filename: "layers.md",
4182
+ content: `# Layers & Composition
4335
4183
 
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
- };
4184
+ Explicit z-index prevents visual bugs.
4345
4185
 
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();
4186
+ ## Z-Index Scale
4353
4187
 
4354
- let x = path[0].x;
4355
- let y = path[0].y;
4188
+ | Layer | Z-Index | Examples |
4189
+ |-------|---------|----------|
4190
+ | Background orbs | 0 | Moving gradients, noise |
4191
+ | Vignette | 1 | Dark corners overlay |
4192
+ | UI Base | 10 | Main UI container |
4193
+ | UI Elements | 20 | Buttons, cards, inputs |
4194
+ | Overlays | 30 | Modals, tooltips, highlights |
4195
+ | Text/Captions | 40 | Titles, labels |
4196
+ | Cursor | 50 | Mouse pointer |
4197
+ | Debug | 100 | Frame counter (remove in final) |
4356
4198
 
4357
- for (let i = 0; i < path.length - 1; i++) {
4358
- const from = path[i];
4359
- const to = path[i + 1];
4199
+ ## Composition Structure
4360
4200
 
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
- });
4201
+ \`\`\`tsx
4202
+ <AbsoluteFill>
4203
+ {/* Background layer */}
4204
+ <MovingBackground />
4205
+
4206
+ {/* Vignette */}
4207
+ <Vignette />
4208
+
4209
+ {/* Camera rig wraps all content */}
4210
+ <CameraRig>
4211
+ <Sequence from={0}>
4212
+ <Scene1 />
4213
+ </Sequence>
4214
+ <Sequence from={SCENE_2_START}>
4215
+ <Scene2 />
4216
+ </Sequence>
4217
+ </CameraRig>
4218
+
4219
+ {/* Audio */}
4220
+ <Audio src={music} volume={0.3} />
4221
+ <Audio src={voiceover} />
4222
+ </AbsoluteFill>
4223
+ \`\`\`
4367
4224
 
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
- }
4225
+ ## Overlapping Sequences
4376
4226
 
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
- ---
4227
+ For zoom-through transitions, scenes overlap:
4405
4228
 
4406
- # Integrating App Components into Remotion
4229
+ \`\`\`tsx
4230
+ <Sequence from={0} durationInFrames={100}>
4231
+ <Scene1 />
4232
+ </Sequence>
4233
+ <Sequence from={80} durationInFrames={100}> {/* 20 frame overlap */}
4234
+ <Scene2 />
4235
+ </Sequence>
4236
+ \`\`\`
4237
+ `
4238
+ },
4239
+ {
4240
+ filename: "project-based.md",
4241
+ content: `# Component Extraction (Critical for Product Videos)
4407
4242
 
4408
- Use your actual React components OR replicate them pixel-perfect in Remotion videos.
4243
+ When creating a video for a project, **copy the actual components** - don't rebuild from scratch.
4409
4244
 
4410
- ## Two Approaches
4245
+ ## The Goal
4411
4246
 
4412
- ### Approach A: Replicate UI (Recommended)
4413
- Read your app's components, note every visual detail, build identical-looking components in Remotion.
4247
+ Your video should show the REAL product UI, pixel-perfect. Viewers should not be able to tell the difference between a screenshot and your video (except yours is animated with kinetic motion).
4414
4248
 
4415
- **Why?** Your app components have hooks, state, and dependencies that don't work in Remotion. Replication is cleaner.
4249
+ ## Process: Eject \u2192 Simplify \u2192 Animate
4416
4250
 
4417
- ### Approach B: Copy Components (When simple enough)
4418
- For truly simple presentational components, you can copy them directly.
4251
+ ### Step 1: Find Components to Feature
4419
4252
 
4420
4253
  \`\`\`bash
4421
- cp -r ../my-app/src/components/Card ./src/app-components/
4422
- cp ../my-app/tailwind.config.js ./
4254
+ # Explore the project structure
4255
+ ls src/components/
4256
+ ls src/app/
4257
+ # Find the key UI: dashboards, forms, cards, modals, etc.
4423
4258
  \`\`\`
4424
4259
 
4425
- ---
4260
+ ### Step 2: Copy Component Code
4426
4261
 
4427
- ## Adapting Components
4262
+ Copy the actual component file into your Remotion project.
4428
4263
 
4429
- ### 1. Remove Interactivity
4264
+ ### Step 3: Eject - Strip Logic, Keep Visuals
4430
4265
 
4431
4266
  \`\`\`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
- \`\`\`
4267
+ // BEFORE: Original component from project
4268
+ export function PricingCard({ plan, onSelect }) {
4269
+ const [loading, setLoading] = useState(false);
4270
+ const handleClick = async () => { /* API calls */ };
4271
+ return <div>...</div>;
4272
+ }
4438
4273
 
4439
- ### 2. Replace Dynamic Data
4274
+ // AFTER: Ejected for Remotion
4275
+ export function PricingCard({ plan, progress, isHovered }) {
4276
+ // No useState, no API calls, no handlers
4277
+ // Just visual + animation props from Remotion
4440
4278
 
4441
- \`\`\`tsx
4442
- // BEFORE (fetches from API)
4443
- const { data } = useQuery('GET_USERS');
4279
+ const entranceProgress = spring({ frame: progress, fps, config: { mass: 2, damping: 20, stiffness: 100 } });
4280
+ const rotateX = interpolate(entranceProgress, [0, 1], [20, 0]);
4444
4281
 
4445
- // AFTER (scripted data)
4446
- const data = [
4447
- { id: 1, name: 'Sarah Chen', role: 'Designer' },
4448
- { id: 2, name: 'Alex Rivera', role: 'Developer' },
4449
- ];
4282
+ return (
4283
+ <div style={{
4284
+ transform: \`perspective(1000px) rotateX(\${rotateX}deg)\`,
4285
+ // ... exact same visual styles as original
4286
+ }}>
4287
+ ...
4288
+ </div>
4289
+ );
4290
+ }
4450
4291
  \`\`\`
4451
4292
 
4452
- ### 3. Wrap with Animation
4293
+ ### Step 4: Add Kinetic Animation
4453
4294
 
4454
- \`\`\`tsx
4455
- import { FadeIn, SlideUp } from '../shared';
4295
+ Apply the rules from kinetic-saas.md:
4296
+ - 2.5D rotation entrance
4297
+ - Spring physics
4298
+ - Staggered children
4299
+ - Cursor interaction states
4456
4300
 
4457
- <FadeIn delay={0}>
4458
- <Navbar />
4459
- </FadeIn>
4301
+ ### Step 5: Fill With Real Demo Data
4460
4302
 
4461
- <SlideUp delay={15}>
4462
- <Sidebar />
4463
- </SlideUp>
4303
+ \`\`\`tsx
4304
+ // Use realistic data that matches the product
4305
+ const DEMO_PLANS = [
4306
+ { name: 'Starter', price: 9, features: ['5 projects', '10GB storage'] },
4307
+ { name: 'Pro', price: 29, features: ['Unlimited projects', '100GB storage', 'Priority support'] },
4308
+ ];
4309
+ // NOT: "Plan A", "$XX/mo", "Feature 1"
4464
4310
  \`\`\`
4465
4311
 
4466
- ---
4467
-
4468
- ## Common Showcase Patterns
4312
+ ## What to Copy
4469
4313
 
4470
- ### Dashboard with Staggered Widgets
4314
+ | Find in Project | Copy to Video |
4315
+ |-----------------|---------------|
4316
+ | \`tailwind.config.js\` | Colors, fonts, spacing |
4317
+ | \`globals.css\` | CSS variables, font imports |
4318
+ | \`/public/logo.svg\` | Actual logo file |
4319
+ | \`/src/components/*.tsx\` | Component structure & styles |
4471
4320
 
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
- };
4492
- \`\`\`
4321
+ ## Common Mistakes
4493
4322
 
4494
- ### Form with Typing Simulation
4323
+ \u274C Using placeholder colors like \`#333\`
4324
+ \u2705 Using exact project colors from tailwind config
4495
4325
 
4496
- \`\`\`tsx
4497
- const FormShowcase = () => {
4498
- const frame = useCurrentFrame();
4499
- const { fps } = useVideoConfig();
4326
+ \u274C Generic content: "Feature 1", "Lorem ipsum"
4327
+ \u2705 Real content: "Unlimited projects", "Priority support"
4500
4328
 
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
- };
4515
- \`\`\`
4329
+ \u274C Building from scratch based on screenshots
4330
+ \u2705 Copying actual component code and ejecting it
4516
4331
 
4517
- ### Modal Slide-In
4332
+ \u274C Static fade-in animations
4333
+ \u2705 2.5D rotation with spring physics (kinetic style)
4334
+ `
4335
+ },
4336
+ {
4337
+ filename: "social-media.md",
4338
+ content: `# Social Media Video Guide (TikTok/Reels/Shorts)
4518
4339
 
4519
- \`\`\`tsx
4520
- const ModalShowcase = () => {
4521
- const frame = useCurrentFrame();
4522
- const showModal = frame > 30;
4340
+ **Platforms:** TikTok, Instagram Reels, YouTube Shorts (all 9:16 vertical)
4523
4341
 
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
- };
4540
- \`\`\`
4342
+ **Philosophy:** "Don't make ads. Make TikToks." Your video must look native to the platform - lo-fi beats polished.
4541
4343
 
4542
4344
  ---
4543
4345
 
4544
- ## Troubleshooting
4346
+ ## 1. The "3-Second War"
4545
4347
 
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
- );
4555
- \`\`\`
4348
+ You have 3 seconds before the thumb scrolls. Win or lose everything.
4556
4349
 
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
- ---
4350
+ ### Hook Requirements (0-3s)
4572
4351
 
4573
- # Project-Based Video Workflow
4352
+ - **Text on screen in FRAME 1** - no waiting for audio
4353
+ - **Movement** - never start static
4354
+ - **Pattern interrupt** - something unexpected
4574
4355
 
4575
- Create promotional videos using **your actual project's UI** replicated in Remotion.
4356
+ ### Hook Types
4576
4357
 
4577
- ## When to Use
4358
+ | Type | Description | Use For |
4359
+ |------|-------------|---------|
4360
+ | Face Close-Up | Tight on face, zoom out | Personal/story |
4361
+ | Green Screen | Speaker + background tweet/article | Commentary |
4362
+ | Text Slam | Bold text hits screen with sound | Listicles, tips |
4363
+ | Movement | Object thrown, quick zoom | Product demos |
4578
4364
 
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
4365
+ ### Audio Hooks
4583
4366
 
4584
- ## Quick Start
4367
+ Bad: "Today I want to talk about..."
4368
+ Good: "Stop scrolling if you hate [X]." / "This feels illegal to know."
4585
4369
 
4586
- \`\`\`bash
4587
- # 1. Scaffold video project
4588
- ${cmd2} video init my-app-promo
4589
- cd my-app-promo
4370
+ ---
4590
4371
 
4591
- # 2. Generate audio assets
4592
- ${cmd2} video create \\
4593
- --script "Introducing our new app..." \\
4594
- --output ./public
4372
+ ## 2. Pacing Rules (No Dead Air)
4595
4373
 
4596
- # 3. Build scenes replicating your app's UI
4374
+ | Rule | Implementation |
4375
+ |------|----------------|
4376
+ | Visual change every 2-3s | Cut, zoom, or new element every 60-90 frames |
4377
+ | No "Millennial Pause" | Never start with 1-2s of stillness |
4378
+ | Remove all breaths | Edit out pauses between sentences |
4379
+ | Jump cuts are native | Don't smooth-cut; jump cuts feel authentic |
4597
4380
 
4598
- # 4. Preview & Render
4599
- npm run dev
4600
- npm run render
4381
+ ---
4382
+
4383
+ ## 3. Native Lo-Fi Aesthetic
4384
+
4385
+ | Looks Like TV Ad (Bad) | Looks Native (Good) |
4386
+ |------------------------|---------------------|
4387
+ | Perfect lighting | Natural/ring light |
4388
+ | Broadcast fonts | Platform-native fonts |
4389
+ | Smooth transitions | Jump cuts |
4390
+ | Color graded | Raw, slightly oversaturated |
4391
+
4392
+ ### Native Font Stack
4393
+
4394
+ \`\`\`tsx
4395
+ const NATIVE_FONT = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif";
4601
4396
  \`\`\`
4602
4397
 
4603
4398
  ---
4604
4399
 
4605
- ## Full Workflow
4400
+ ## 4. Safe Zone Rules (CRITICAL)
4606
4401
 
4607
- ### Step 1: Analyze Project
4402
+ Platform UI overlays your content. Plan for it.
4608
4403
 
4609
- \`\`\`bash
4610
- # Check framework
4611
- cat package.json | grep -E "react|next|vue"
4404
+ \`\`\`
4405
+ 1080x1920 Canvas
4406
+ +------------------------------------------+
4407
+ | TOP SAFE ZONE (150px) | <- Notch, status bar
4408
+ | ----------------------------------------|
4409
+ | |
4410
+ | CONTENT SAFE ZONE |
4411
+ | (60px left, 120px right margins) |
4412
+ | |
4413
+ | ----------------------------------------|
4414
+ | BOTTOM SAFE ZONE (384px / 20%) | <- Captions, username
4415
+ | [Icon strip on right: 120px] | <- Like, Comment, Share
4416
+ +------------------------------------------+
4417
+ \`\`\`
4418
+
4419
+ ### Safe Zone Values
4612
4420
 
4613
- # List components
4614
- ls -la src/components/
4421
+ \`\`\`tsx
4422
+ const SAFE_ZONE = {
4423
+ top: 150, // Notch/status bar
4424
+ bottom: 384, // 20% of 1920 - captions/username
4425
+ left: 60, // Edge safety
4426
+ right: 120, // Like/Comment/Share icons
4427
+ };
4615
4428
 
4616
- # Get colors
4617
- cat tailwind.config.* | grep -A 30 "colors"
4429
+ // Captions must sit ABOVE bottom safe zone
4430
+ const CAPTION_BOTTOM = 350; // px from bottom
4618
4431
  \`\`\`
4619
4432
 
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
4433
+ ### SafeZoneGuide Component
4434
+
4435
+ \`\`\`tsx
4436
+ const SafeZoneGuide: React.FC = () => (
4437
+ <AbsoluteFill style={{ pointerEvents: "none" }}>
4438
+ {/* Top danger zone */}
4439
+ <div style={{
4440
+ position: "absolute", top: 0, left: 0, right: 0, height: 150,
4441
+ backgroundColor: "rgba(255,0,0,0.2)", borderBottom: "2px dashed red",
4442
+ }} />
4443
+ {/* Bottom danger zone */}
4444
+ <div style={{
4445
+ position: "absolute", bottom: 0, left: 0, right: 0, height: 384,
4446
+ backgroundColor: "rgba(255,0,0,0.2)", borderTop: "2px dashed red",
4447
+ }} />
4448
+ {/* Right icon strip */}
4449
+ <div style={{
4450
+ position: "absolute", top: 150, right: 0, bottom: 384, width: 120,
4451
+ backgroundColor: "rgba(255,165,0,0.2)", borderLeft: "2px dashed orange",
4452
+ }} />
4453
+ </AbsoluteFill>
4454
+ );
4455
+ \`\`\`
4625
4456
 
4626
- ### Step 2: Document Brand
4457
+ ---
4627
4458
 
4628
- \`\`\`markdown
4629
- ## Brand: [App Name]
4459
+ ## 5. Content Formats
4630
4460
 
4631
- ### Colors (from tailwind.config)
4632
- - Background: #0f172a
4633
- - Surface: #1e293b
4634
- - Primary: #14b8a6
4635
- - Text: #ffffff
4461
+ ### Green Screen (Speaker + Background)
4636
4462
 
4637
- ### Key Components
4638
- 1. Sidebar - Dark bg, navigation items
4639
- 2. Dashboard - Stats cards, charts
4640
- 3. Modal - Overlay, card
4463
+ \`\`\`tsx
4464
+ <AbsoluteFill>
4465
+ <Img src={backgroundImage} style={{ width: "100%", height: "100%", objectFit: "cover" }} />
4466
+ <div style={{
4467
+ position: "absolute", bottom: 400, left: 40,
4468
+ width: 300, height: 400, borderRadius: 20, overflow: "hidden",
4469
+ }}>
4470
+ <Video src={speakerVideo} />
4471
+ </div>
4472
+ </AbsoluteFill>
4641
4473
  \`\`\`
4642
4474
 
4643
- ### Step 3: Plan Scenes
4475
+ ### Listicle (Rapid-Fire)
4644
4476
 
4645
- \`\`\`markdown
4646
- ## Scene Plan
4477
+ \`\`\`tsx
4478
+ const FRAMES_PER_ITEM = 60; // 2 seconds per item
4479
+ const currentIndex = Math.floor(frame / FRAMES_PER_ITEM);
4480
+ // Slam-in with spring animation for each item
4481
+ \`\`\`
4647
4482
 
4648
- ### Scene 1: Intro (3s)
4649
- - Logo centered
4650
- - Tagline fades up
4483
+ ### Visual ASMR (No Talking)
4651
4484
 
4652
- ### Scene 2: Dashboard (5s)
4653
- - Stats widgets stagger in
4654
- - Chart animates
4485
+ - No voiceover, music only
4486
+ - Slow, deliberate cursor movements
4487
+ - Satisfying click feedback
4488
+ - Ken Burns zoom on details
4655
4489
 
4656
- ### Scene 3: Feature Demo (5s)
4657
- - Sidebar slides in
4658
- - Selection animates
4490
+ ### POV Skit
4659
4491
 
4660
- ### Scene 4: CTA (3s)
4661
- - Logo + button
4492
+ \`\`\`tsx
4493
+ <div style={{ position: "absolute", top: 160, left: 40, right: 120, fontSize: 42 }}>
4494
+ POV: {setup}
4495
+ </div>
4662
4496
  \`\`\`
4663
4497
 
4664
- ### Step 4: Build Scenes
4498
+ ---
4499
+
4500
+ ## 6. TikTok Captions
4665
4501
 
4666
- Create scenes in \`src/remotion/scenes/\` that replicate your UI:
4502
+ Use \`@remotion/captions\` with \`createTikTokStyleCaptions\`:
4667
4503
 
4668
4504
  \`\`\`tsx
4669
- // src/remotion/scenes/DashboardScene.tsx
4670
- import { AbsoluteFill, useCurrentFrame, spring, useVideoConfig } from "remotion";
4671
-
4672
- export const DASHBOARD_SCENE_DURATION = 150;
4505
+ import { createTikTokStyleCaptions } from "@remotion/captions";
4673
4506
 
4674
- const mockData = {
4675
- revenue: 125000,
4676
- users: 1234,
4677
- orders: 567,
4678
- };
4507
+ const { pages } = createTikTokStyleCaptions({
4508
+ captions: transcriptCaptions,
4509
+ combineTokensWithinMilliseconds: 1200, // Higher = more words per page
4510
+ });
4511
+ \`\`\`
4679
4512
 
4680
- export const DashboardScene: React.FC = () => {
4681
- const frame = useCurrentFrame();
4682
- const { fps } = useVideoConfig();
4513
+ ### Word-by-Word Highlighting
4683
4514
 
4515
+ \`\`\`tsx
4516
+ {page.tokens.map((token) => {
4517
+ const isActive = currentTimeMs >= token.fromMs && currentTimeMs < token.toMs;
4684
4518
  return (
4685
- <AbsoluteFill style={{ backgroundColor: "#0f172a", padding: 40 }}>
4686
- {/* Replicate your dashboard layout here */}
4687
- {/* Use EXACT colors from your tailwind.config */}
4688
- </AbsoluteFill>
4519
+ <span style={{
4520
+ color: isActive ? "#FFD700" : "#fff",
4521
+ fontSize: 64,
4522
+ fontWeight: 900,
4523
+ textTransform: "uppercase",
4524
+ }}>
4525
+ {token.text}
4526
+ </span>
4689
4527
  );
4690
- };
4528
+ })}
4691
4529
  \`\`\`
4692
4530
 
4693
- ### Step 5: Generate Audio
4531
+ ---
4694
4532
 
4695
- \`\`\`bash
4696
- ${cmd2} video create \\
4697
- --script "Introducing [App]. The fastest way to..." \\
4698
- --music-prompt "modern uplifting tech" \\
4699
- --output ./public
4700
- \`\`\`
4533
+ ## 7. Script Structure (60 Seconds)
4701
4534
 
4702
- ### Step 6: Render
4535
+ | Timestamp | Section | Purpose |
4536
+ |-----------|---------|---------|
4537
+ | 0:00-0:03 | **HOOK** | Pattern interrupt, bold claim |
4538
+ | 0:03-0:10 | **PROBLEM** | Agitate the pain point |
4539
+ | 0:10-0:40 | **SOLUTION** | Rapid value delivery |
4540
+ | 0:40-0:55 | **PROOF** | Show, don't tell |
4541
+ | 0:55-0:60 | **CTA** | Soft ask (Follow/Link in bio) |
4703
4542
 
4704
- \`\`\`bash
4705
- npm run dev # Preview
4706
- npm run render # Final video
4707
- \`\`\`
4543
+ ### CTA Best Practices
4544
+
4545
+ | Do | Don't |
4546
+ |----|-------|
4547
+ | "Follow for more tips" | "SMASH that like button" |
4548
+ | "Link in bio" | "Click the link below" |
4549
+ | "Save this for later" | "Subscribe to my channel" |
4708
4550
 
4709
4551
  ---
4710
4552
 
4711
- ## Tips
4553
+ ## 8. Quick Reference
4712
4554
 
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
- `;
4719
- }
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 = [
4733
- { name: "Claude Code", dir: ".claude" },
4734
- { name: "Cursor", dir: ".cursor" },
4735
- { name: "Codex", dir: ".codex" },
4736
- { name: "OpenCode", dir: ".opencode" },
4737
- { name: "Windsurf", dir: ".windsurf" },
4738
- { name: "Agent", dir: ".agent" }
4739
- ];
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
4555
+ ### Platform Specs
4746
4556
 
4747
- ${chalk12.gray("# Install to specific directory")}
4748
- $ ${brand.name} skill install --dir ~/.claude
4557
+ | Platform | Ratio | Resolution | Duration | FPS |
4558
+ |----------|-------|------------|----------|-----|
4559
+ | TikTok | 9:16 | 1080x1920 | 15-60s | 30 |
4560
+ | Reels | 9:16 | 1080x1920 | 15-90s | 30 |
4561
+ | Shorts | 9:16 | 1080x1920 | 15-60s | 30 |
4562
+ | YouTube | 16:9 | 1920x1080 | any | 30/60 |
4749
4563
 
4750
- ${chalk12.gray("# Install without remotion-best-practices")}
4751
- $ ${brand.name} skill install --skip-remotion
4564
+ ### Timing (30fps)
4752
4565
 
4753
- ${chalk12.gray("# Show skill content")}
4754
- $ ${brand.name} skill show
4566
+ | Action | Frames | Seconds |
4567
+ |--------|--------|---------|
4568
+ | Hook impact | 0-3 | 0-0.1s |
4569
+ | Text hold min | 45 | 1.5s |
4570
+ | Scene change max | 90 | 3s |
4571
+ | Ideal scene | 60 | 2s |
4572
+
4573
+ ### Self-Check
4574
+
4575
+ - [ ] Text in FRAME 1 (no millennial pause)
4576
+ - [ ] Visual change every 2-3 seconds
4577
+ - [ ] All content within safe zones
4578
+ - [ ] Captions at bottom: 350px+
4579
+ - [ ] No content in bottom 20% or right 120px
4580
+ - [ ] Lo-fi aesthetic (not TV ad)
4581
+ - [ ] CTA is soft, not desperate
4582
+ - [ ] Duration 15-60 seconds
4755
4583
  `
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 = [];
4584
+ }
4585
+ ];
4586
+
4587
+ // src/commands/skill/installer.ts
4588
+ function validatePath(basePath, targetPath) {
4589
+ const resolvedBase = resolve4(basePath);
4590
+ const resolvedTarget = resolve4(basePath, targetPath);
4591
+ const relativePath = relative(resolvedBase, resolvedTarget);
4592
+ if (relativePath.startsWith("..") || resolve4(resolvedTarget) !== resolvedTarget.replace(/\.\./g, "")) {
4593
+ throw new Error(`Invalid path: "${targetPath}" would escape base directory`);
4594
+ }
4595
+ return resolvedTarget;
4596
+ }
4597
+ function getRuleFiles() {
4598
+ const rules = [];
4599
+ for (const rule of VIDEO_RULE_CONTENTS) {
4600
+ rules.push({
4601
+ path: `rules/video/${rule.filename}`,
4602
+ content: rule.content
4603
+ });
4604
+ }
4605
+ return rules;
4606
+ }
4607
+ function installSkillToPath(skillPath, content) {
4608
+ const skillFile = join(skillPath, "SKILL.md");
4609
+ mkdirSync(skillPath, { recursive: true });
4610
+ writeFileSync(skillFile, content, "utf-8");
4611
+ const rules = getRuleFiles();
4612
+ for (const rule of rules) {
4613
+ const rulePath = join(skillPath, rule.path);
4614
+ const ruleDir = join(skillPath, rule.path.split("/").slice(0, -1).join("/"));
4615
+ mkdirSync(ruleDir, { recursive: true });
4616
+ writeFileSync(rulePath, rule.content, "utf-8");
4617
+ }
4618
+ }
4619
+ function installSkill(skillName, content, options = {}) {
4620
+ const result = {
4621
+ installed: [],
4622
+ skipped: [],
4623
+ errors: []
4624
+ };
4761
4625
  const baseDir = options.local ? process.cwd() : homedir();
4762
- const skillFiles = generateAllSkillFiles(brand);
4763
4626
  if (options.dir) {
4764
- const skillPath = join(options.dir, "skills", brand.name);
4765
4627
  try {
4766
- installSkill(skillPath, skillFiles, options.force);
4767
- installed.push(options.dir);
4628
+ const resolvedDir = resolve4(options.dir);
4629
+ const skillPath = validatePath(resolvedDir, join("skills", skillName));
4630
+ installSkillToPath(skillPath, content);
4631
+ result.installed.push(options.dir);
4768
4632
  } catch (err) {
4769
- errors.push(`${options.dir}: ${err instanceof Error ? err.message : String(err)}`);
4633
+ result.errors.push(`${options.dir}: ${err instanceof Error ? err.message : String(err)}`);
4770
4634
  }
4771
4635
  } else {
4772
- for (const editor of EDITORS) {
4636
+ for (const editor of SUPPORTED_EDITORS) {
4773
4637
  const editorDir = join(baseDir, editor.dir);
4774
- const skillPath = join(editorDir, "skills", brand.name);
4638
+ const skillPath = join(editorDir, "skills", skillName);
4775
4639
  const skillFile = join(skillPath, "SKILL.md");
4776
4640
  if (!existsSync2(editorDir)) {
4777
4641
  continue;
4778
4642
  }
4779
4643
  if (existsSync2(skillFile) && !options.force) {
4780
- skipped.push(editor.name);
4644
+ result.skipped.push(editor.name);
4781
4645
  continue;
4782
4646
  }
4783
4647
  try {
4784
- installSkill(skillPath, skillFiles, options.force);
4785
- installed.push(editor.name);
4648
+ installSkillToPath(skillPath, content);
4649
+ result.installed.push(editor.name);
4650
+ } catch (err) {
4651
+ result.errors.push(`${editor.name}: ${err instanceof Error ? err.message : String(err)}`);
4652
+ }
4653
+ }
4654
+ }
4655
+ return result;
4656
+ }
4657
+ function uninstallSkill(skillName, options = {}) {
4658
+ const result = {
4659
+ removed: [],
4660
+ errors: []
4661
+ };
4662
+ const baseDir = options.local ? process.cwd() : homedir();
4663
+ for (const editor of SUPPORTED_EDITORS) {
4664
+ const skillPath = join(baseDir, editor.dir, "skills", skillName);
4665
+ if (existsSync2(skillPath)) {
4666
+ try {
4667
+ rmSync(skillPath, { recursive: true });
4668
+ result.removed.push(editor.name);
4786
4669
  } catch (err) {
4787
- errors.push(`${editor.name}: ${err instanceof Error ? err.message : String(err)}`);
4670
+ result.errors.push(`${editor.name}: ${err instanceof Error ? err.message : String(err)}`);
4788
4671
  }
4789
4672
  }
4790
4673
  }
4674
+ return result;
4675
+ }
4676
+ function getSupportedEditorNames() {
4677
+ return SUPPORTED_EDITORS.map((e) => e.name);
4678
+ }
4679
+
4680
+ // src/commands/skill/index.ts
4681
+ var skillCommand = new Command14("skill").description(`Manage ${brand.displayName} skill for AI coding assistants`).addHelpText(
4682
+ "after",
4683
+ `
4684
+ ${chalk12.bold("Examples:")}
4685
+ ${chalk12.gray("# Install skill for all detected editors")}
4686
+ $ ${brand.name} skill install
4687
+
4688
+ ${chalk12.gray("# Install to specific directory")}
4689
+ $ ${brand.name} skill install --dir ~/.claude
4690
+
4691
+ ${chalk12.gray("# Show skill content")}
4692
+ $ ${brand.name} skill show
4693
+ `
4694
+ );
4695
+ skillCommand.command("install").description(`Install the ${brand.displayName} skill for AI coding assistants`).option("-d, --dir <path>", "Install to specific directory").option("-g, --global", "Install globally (to home directory)", true).option("-l, --local", "Install locally (to current directory)").option("-f, --force", "Overwrite existing skill files").action(async (options) => {
4696
+ const skillContent = generateSkillContent(brand);
4697
+ const result = installSkill(brand.name, skillContent, {
4698
+ dir: options.dir,
4699
+ local: options.local,
4700
+ force: options.force
4701
+ });
4791
4702
  console.log();
4792
- if (installed.length > 0) {
4703
+ if (result.installed.length > 0) {
4793
4704
  success("Skill installed successfully");
4794
4705
  console.log();
4795
- keyValue("Installed to", installed.join(", "));
4796
- keyValue("Files", Object.keys(skillFiles).length.toString());
4706
+ keyValue("Installed to", result.installed.join(", "));
4797
4707
  }
4798
- if (skipped.length > 0) {
4708
+ if (result.skipped.length > 0) {
4799
4709
  console.log();
4800
- info(`Skipped (already exists): ${skipped.join(", ")}`);
4710
+ info(`Skipped (already exists): ${result.skipped.join(", ")}`);
4801
4711
  console.log(chalk12.gray(" Use --force to overwrite"));
4802
4712
  }
4803
- if (errors.length > 0) {
4713
+ if (result.errors.length > 0) {
4804
4714
  console.log();
4805
- for (const err of errors) {
4715
+ for (const err of result.errors) {
4806
4716
  error(err);
4807
4717
  }
4808
4718
  }
4809
- if (installed.length === 0 && skipped.length === 0 && errors.length === 0) {
4719
+ if (result.installed.length === 0 && result.skipped.length === 0 && result.errors.length === 0) {
4810
4720
  info("No supported AI coding assistants detected.");
4811
4721
  console.log();
4812
- console.log(chalk12.gray("Supported editors: " + EDITORS.map((e) => e.name).join(", ")));
4722
+ console.log(chalk12.gray("Supported editors: " + getSupportedEditorNames().join(", ")));
4813
4723
  console.log(chalk12.gray("Use --dir <path> to install to a specific directory"));
4814
4724
  }
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"));
4827
- }
4828
- }
4829
4725
  console.log();
4830
4726
  });
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
- }
4840
- } else {
4841
- console.log(files["SKILL.md"]);
4842
- console.log(chalk12.gray("\nUse --all to show all files"));
4843
- }
4727
+ skillCommand.command("show").description("Display the skill content").action(() => {
4728
+ console.log(generateSkillContent(brand));
4844
4729
  });
4845
4730
  skillCommand.command("uninstall").description(`Remove the ${brand.displayName} skill from AI coding assistants`).option("-g, --global", "Uninstall globally (from home directory)", true).option("-l, --local", "Uninstall locally (from current directory)").action(async (options) => {
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
- }
4858
- }
4731
+ const result = uninstallSkill(brand.name, { local: options.local });
4859
4732
  console.log();
4860
- if (removed.length > 0) {
4733
+ if (result.removed.length > 0) {
4861
4734
  success("Skill uninstalled");
4862
- keyValue("Removed from", removed.join(", "));
4735
+ keyValue("Removed from", result.removed.join(", "));
4863
4736
  } else {
4864
4737
  info("No installed skills found");
4865
4738
  }
4739
+ if (result.errors.length > 0) {
4740
+ for (const err of result.errors) {
4741
+ warn(`Failed to remove: ${err}`);
4742
+ }
4743
+ }
4866
4744
  console.log();
4867
4745
  });
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
4746
 
4877
4747
  // src/commands/tts.ts
4878
4748
  init_api();
@@ -5300,8 +5170,8 @@ init_types();
5300
5170
  import { Command as Command19 } from "commander";
5301
5171
  import ora12 from "ora";
5302
5172
  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";
5173
+ import { join as join2, resolve as resolve5 } from "path";
5174
+ import { execSync, spawn } from "child_process";
5305
5175
  var DEFAULT_TEMPLATE = "inizio-inc/remotion-composition";
5306
5176
  var DEFAULT_FPS = 30;
5307
5177
  function parseScriptIntoSections(script) {
@@ -5391,6 +5261,26 @@ function calculateSectionTimingFromTimestamps(sections, timestamps, fps) {
5391
5261
  }
5392
5262
  return results;
5393
5263
  }
5264
+ async function readStdin2() {
5265
+ if (process.stdin.isTTY) {
5266
+ return null;
5267
+ }
5268
+ return new Promise((resolve6) => {
5269
+ let data = "";
5270
+ process.stdin.setEncoding("utf-8");
5271
+ process.stdin.on("data", (chunk) => {
5272
+ data += chunk;
5273
+ });
5274
+ process.stdin.on("end", () => resolve6(data.trim() || null));
5275
+ process.stdin.on("error", () => resolve6(null));
5276
+ setTimeout(() => {
5277
+ if (!data) resolve6(null);
5278
+ }, 100);
5279
+ });
5280
+ }
5281
+ function toFilename(name) {
5282
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
5283
+ }
5394
5284
  async function downloadFile3(url, outputPath) {
5395
5285
  if (url.startsWith("data:")) {
5396
5286
  const matches = url.match(/^data:[^;]+;base64,(.+)$/);
@@ -5420,72 +5310,199 @@ function getExtension(url) {
5420
5310
  }
5421
5311
  return "jpg";
5422
5312
  }
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) => {
5313
+ var createCommand3 = new Command19("create").description("Create video assets (voiceover per scene, music, images)").option("-s, --script <text>", "Narration script (legacy single-script mode)").option("--script-file <path>", "Path to script file (legacy) or scenes JSON").option("-t, --topic <text>", "Topic for image search").option("-v, --voice <name>", "TTS voice (Kore, Puck, Rachel, alloy)", "Kore").option("-m, --music-prompt <text>", "Music description").option("-n, --num-images <number>", "Number of images to search/download", "5").option("-o, --output <dir>", "Output directory", "./public").option("-f, --format <format>", "Output format: human, json, quiet", "human").action(async (options) => {
5424
5314
  const format = options.format;
5425
5315
  const spinner = format === "human" ? ora12("Initializing...").start() : null;
5426
5316
  try {
5427
- let script = options.script;
5428
- if (options.scriptFile) {
5317
+ const stdinData = await readStdin2();
5318
+ let scenesInput = null;
5319
+ if (stdinData) {
5429
5320
  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);
5321
+ const parsed = JSON.parse(stdinData);
5322
+ if (parsed.scenes && Array.isArray(parsed.scenes)) {
5323
+ scenesInput = parsed;
5324
+ }
5325
+ } catch {
5435
5326
  }
5436
5327
  }
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);
5328
+ if (!scenesInput && options.scriptFile) {
5329
+ try {
5330
+ const fileContent = await readFile2(options.scriptFile, "utf-8");
5331
+ const parsed = JSON.parse(fileContent);
5332
+ if (parsed.scenes && Array.isArray(parsed.scenes)) {
5333
+ scenesInput = parsed;
5334
+ }
5335
+ } catch {
5336
+ }
5449
5337
  }
5338
+ const voice = scenesInput?.voice || options.voice;
5339
+ const musicPrompt = scenesInput?.musicPrompt || options.musicPrompt || "uplifting background music, positive energy";
5450
5340
  const audioDir = join2(options.output, "audio");
5451
5341
  const imagesDir = join2(options.output, "images");
5342
+ const videosDir = join2(options.output, "videos");
5452
5343
  if (spinner) spinner.text = "Creating directories...";
5453
5344
  await mkdir(audioDir, { recursive: true });
5454
5345
  await mkdir(imagesDir, { recursive: true });
5346
+ await mkdir(videosDir, { recursive: true });
5455
5347
  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();
5348
+ let scenes = [];
5349
+ let totalDuration = 0;
5350
+ const allImages = [];
5351
+ const allVideos = [];
5352
+ if (scenesInput && scenesInput.scenes.length > 0) {
5353
+ if (format === "human") {
5354
+ spinner?.stop();
5355
+ info(`Processing ${scenesInput.scenes.length} scenes...`);
5356
+ spinner?.start();
5357
+ }
5358
+ let currentTime = 0;
5359
+ for (let i = 0; i < scenesInput.scenes.length; i++) {
5360
+ const scene = scenesInput.scenes[i];
5361
+ const filename = toFilename(scene.name);
5362
+ if (spinner) spinner.text = `[${scene.name}] Generating speech...`;
5363
+ const ttsResult = await generateSpeech({
5364
+ text: scene.script,
5365
+ options: { voice }
5366
+ });
5367
+ const audioPath = join2(audioDir, `${filename}.${ttsResult.format}`);
5368
+ await writeFile5(audioPath, ttsResult.audioData);
5369
+ totalCost += ttsResult.cost;
5370
+ const durationInSeconds = ttsResult.duration;
5371
+ const durationInFrames = Math.round(durationInSeconds * DEFAULT_FPS);
5372
+ const sceneData = {
5373
+ id: i + 1,
5374
+ name: scene.name,
5375
+ text: scene.script,
5376
+ wordCount: scene.script.split(/\s+/).length,
5377
+ startTime: currentTime,
5378
+ endTime: currentTime + durationInSeconds,
5379
+ durationInSeconds,
5380
+ durationInFrames,
5381
+ audioPath: `audio/${filename}.${ttsResult.format}`
5382
+ };
5383
+ if (scene.imageQuery) {
5384
+ if (spinner) spinner.text = `[${scene.name}] Searching image...`;
5385
+ try {
5386
+ const imageResults = await searchImages({
5387
+ query: scene.imageQuery,
5388
+ options: { maxResults: 1, size: "large", safeSearch: true }
5389
+ });
5390
+ const imgs = imageResults.data.results.flatMap((r) => r.results);
5391
+ totalCost += imageResults.data.totalCost;
5392
+ if (imgs.length > 0) {
5393
+ const img = imgs[0];
5394
+ const ext = getExtension(img.url);
5395
+ const imgFilename = `${filename}.${ext}`;
5396
+ const imgPath = join2(imagesDir, imgFilename);
5397
+ await downloadFile3(img.url, imgPath);
5398
+ sceneData.imagePath = `images/${imgFilename}`;
5399
+ allImages.push({
5400
+ path: `images/${imgFilename}`,
5401
+ url: img.url,
5402
+ width: img.width,
5403
+ height: img.height,
5404
+ query: scene.imageQuery
5405
+ });
5406
+ }
5407
+ } catch (err) {
5408
+ if (format === "human") {
5409
+ spinner?.stop();
5410
+ warn(`[${scene.name}] Image search failed: ${err instanceof Error ? err.message : "Unknown"}`);
5411
+ spinner?.start();
5412
+ }
5413
+ }
5414
+ }
5415
+ if (scene.videoQuery) {
5416
+ if (spinner) spinner.text = `[${scene.name}] Searching video...`;
5417
+ try {
5418
+ const videoResults = await searchVideos({
5419
+ query: scene.videoQuery,
5420
+ options: { maxResults: 1 }
5421
+ });
5422
+ const vids = videoResults.data.results.flatMap((r) => r.results);
5423
+ totalCost += videoResults.data.totalCost;
5424
+ if (vids.length > 0) {
5425
+ const vid = vids[0];
5426
+ const vidUrl = vid.previewUrl || vid.downloadUrl;
5427
+ if (vidUrl) {
5428
+ const vidFilename = `${filename}.mp4`;
5429
+ const vidPath = join2(videosDir, vidFilename);
5430
+ await downloadFile3(vidUrl, vidPath);
5431
+ sceneData.videoPath = `videos/${vidFilename}`;
5432
+ allVideos.push({
5433
+ path: `videos/${vidFilename}`,
5434
+ url: vidUrl,
5435
+ width: vid.width,
5436
+ height: vid.height,
5437
+ duration: vid.duration,
5438
+ query: scene.videoQuery
5439
+ });
5440
+ }
5441
+ }
5442
+ } catch (err) {
5443
+ if (format === "human") {
5444
+ spinner?.stop();
5445
+ warn(`[${scene.name}] Video search failed: ${err instanceof Error ? err.message : "Unknown"}`);
5446
+ spinner?.start();
5447
+ }
5448
+ }
5449
+ }
5450
+ scenes.push(sceneData);
5451
+ currentTime += durationInSeconds;
5452
+ totalDuration += durationInSeconds;
5453
+ if (format === "human") {
5454
+ spinner?.stop();
5455
+ const assets = [
5456
+ `audio: ${durationInSeconds.toFixed(1)}s`,
5457
+ sceneData.imagePath ? "image" : null,
5458
+ sceneData.videoPath ? "video" : null
5459
+ ].filter(Boolean).join(", ");
5460
+ success(` ${scene.name}: ${assets}`);
5461
+ spinner?.start();
5462
+ }
5463
+ }
5464
+ } else {
5465
+ let script = options.script;
5466
+ if (options.scriptFile) {
5467
+ try {
5468
+ script = await readFile2(options.scriptFile, "utf-8");
5469
+ } catch (err) {
5470
+ spinner?.stop();
5471
+ error(`Failed to read script file: ${err instanceof Error ? err.message : "Unknown error"}`);
5472
+ process.exit(EXIT_CODES.INVALID_INPUT);
5473
+ }
5474
+ }
5475
+ if (!script || script.trim().length === 0) {
5476
+ spinner?.stop();
5477
+ error("Provide scenes via stdin JSON, --script-file with scenes JSON, or --script for legacy mode");
5478
+ process.exit(EXIT_CODES.INVALID_INPUT);
5479
+ }
5480
+ script = script.trim();
5481
+ const _topic = options.topic || script.split(".")[0].slice(0, 50);
5482
+ void _topic;
5483
+ if (spinner) spinner.text = "Generating voiceover...";
5484
+ const ttsResult = await generateSpeech({
5485
+ text: script,
5486
+ options: { voice }
5487
+ });
5488
+ const voiceoverPath = join2(audioDir, `voiceover.${ttsResult.format}`);
5489
+ await writeFile5(voiceoverPath, ttsResult.audioData);
5490
+ totalCost += ttsResult.cost;
5491
+ totalDuration = ttsResult.duration;
5492
+ const sectionTexts = parseScriptIntoSections(script);
5493
+ const sectionsWithTiming = calculateSectionTiming(sectionTexts, ttsResult.duration, DEFAULT_FPS, ttsResult.timestamps);
5494
+ scenes = sectionsWithTiming.map((s, i) => ({
5495
+ ...s,
5496
+ name: `Section${i + 1}`,
5497
+ audioPath: `audio/voiceover.${ttsResult.format}`
5498
+ }));
5499
+ if (format === "human") {
5500
+ spinner?.stop();
5501
+ success(`Voiceover: ${voiceoverPath} (${ttsResult.duration.toFixed(1)}s)`);
5502
+ spinner?.start();
5503
+ }
5486
5504
  }
5487
- const musicDuration = Math.min(30, Math.ceil(ttsResult.duration) + 5);
5488
- const musicPrompt = options.musicPrompt || "uplifting background music, positive energy";
5505
+ const musicDuration = Math.min(30, Math.ceil(totalDuration) + 5);
5489
5506
  if (spinner) spinner.text = "Generating music...";
5490
5507
  let musicResult = await generateMusic({
5491
5508
  prompt: musicPrompt,
@@ -5520,67 +5537,13 @@ var createCommand2 = new Command19("create").description("Create video assets (v
5520
5537
  success(`Music: ${musicPath} (${musicInfo.duration}s)`);
5521
5538
  spinner?.start();
5522
5539
  }
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
- }
5561
- }
5562
- }
5563
- if (format === "human") {
5564
- spinner?.stop();
5565
- success(`Images: Downloaded ${downloadedImages.length} images to ${imagesDir}`);
5566
- spinner?.start();
5567
- }
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
5540
  if (spinner) spinner.text = "Writing manifest...";
5576
- const totalDurationInFrames = Math.round(ttsResult.duration * DEFAULT_FPS);
5541
+ const totalDurationInFrames = Math.round(totalDuration * DEFAULT_FPS);
5577
5542
  const manifest = {
5578
- topic,
5579
- script,
5580
- voiceover: voiceoverInfo,
5581
5543
  music: musicInfo,
5582
- images: downloadedImages,
5583
- sections: sectionsWithImages,
5544
+ images: allImages,
5545
+ videos: allVideos,
5546
+ scenes,
5584
5547
  totalDurationInFrames,
5585
5548
  fps: DEFAULT_FPS,
5586
5549
  totalCost,
@@ -5600,19 +5563,19 @@ var createCommand2 = new Command19("create").description("Create video assets (v
5600
5563
  console.log();
5601
5564
  success("Video assets created successfully!");
5602
5565
  console.log();
5603
- info(`Topic: ${topic}`);
5604
- info(`Voiceover: ${voiceoverInfo.path} (${voiceoverInfo.duration.toFixed(1)}s, ${voiceoverInfo.voice})`);
5566
+ info(`Scenes: ${scenes.length} (${totalDurationInFrames} frames at ${DEFAULT_FPS}fps)`);
5567
+ for (const scene of scenes) {
5568
+ const assets = [
5569
+ scene.audioPath ? "audio" : null,
5570
+ scene.imagePath ? "image" : null,
5571
+ scene.videoPath ? "video" : null
5572
+ ].filter(Boolean).join(", ");
5573
+ info(` - ${scene.name}: ${scene.durationInSeconds.toFixed(1)}s [${assets}]`);
5574
+ }
5605
5575
  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
5576
  info(`Manifest: ${manifestPath}`);
5609
5577
  console.log();
5610
5578
  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
5579
  } catch (err) {
5617
5580
  spinner?.stop();
5618
5581
  error(err instanceof Error ? err.message : "Unknown error");
@@ -5663,11 +5626,11 @@ var searchCommand2 = new Command19("search").description("Search for stock video
5663
5626
  process.exit(EXIT_CODES.GENERAL_ERROR);
5664
5627
  }
5665
5628
  });
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) => {
5629
+ 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
5630
  const format = options.format;
5668
5631
  const spinner = format === "human" ? ora12("Initializing video project...").start() : null;
5669
5632
  try {
5670
- const targetDir = resolve4(process.cwd(), name);
5633
+ const targetDir = resolve5(process.cwd(), name);
5671
5634
  try {
5672
5635
  await access(targetDir);
5673
5636
  spinner?.stop();
@@ -5675,14 +5638,22 @@ var initCommand = new Command19("init").description("Create a new Remotion video
5675
5638
  process.exit(EXIT_CODES.INVALID_INPUT);
5676
5639
  } catch {
5677
5640
  }
5641
+ const templatePattern = /^[a-zA-Z0-9_-]+\/[a-zA-Z0-9_.-]+(\/[a-zA-Z0-9_.-]+)*(#[a-zA-Z0-9_.-]+)?$/;
5642
+ if (!templatePattern.test(options.template)) {
5643
+ spinner?.stop();
5644
+ error(`Invalid template format: "${options.template}". Expected format: owner/repo or owner/repo#branch`);
5645
+ process.exit(EXIT_CODES.INVALID_INPUT);
5646
+ }
5678
5647
  if (spinner) spinner.text = `Downloading template from ${options.template}...`;
5679
5648
  try {
5680
- execSync2(`npx --yes degit ${options.template} "${targetDir}"`, {
5649
+ execSync(`npx --yes degit ${options.template} "${targetDir}"`, {
5681
5650
  stdio: "pipe"
5682
5651
  });
5683
5652
  } catch {
5684
5653
  if (spinner) spinner.text = "Cloning template...";
5685
- execSync2(`git clone --depth 1 https://github.com/${options.template}.git "${targetDir}"`, {
5654
+ const repoMatch = options.template.match(/^([a-zA-Z0-9_-]+\/[a-zA-Z0-9_.-]+)/);
5655
+ const repo = repoMatch ? repoMatch[1] : options.template;
5656
+ execSync(`git clone --depth 1 https://github.com/${repo}.git "${targetDir}"`, {
5686
5657
  stdio: "pipe"
5687
5658
  });
5688
5659
  await rm(join2(targetDir, ".git"), { recursive: true, force: true });
@@ -5715,12 +5686,37 @@ var initCommand = new Command19("init").description("Create a new Remotion video
5715
5686
  spinner?.start();
5716
5687
  }
5717
5688
  }
5689
+ if (options.type === "tiktok") {
5690
+ if (spinner) spinner.text = "Configuring for TikTok (9:16)...";
5691
+ const constantsPath = join2(targetDir, "types/constants.ts");
5692
+ try {
5693
+ let constantsContent = await readFile2(constantsPath, "utf-8");
5694
+ constantsContent = constantsContent.replace(
5695
+ /export const VIDEO_WIDTH = 1920;/,
5696
+ "export const VIDEO_WIDTH = 1080; // TikTok 9:16"
5697
+ ).replace(
5698
+ /export const VIDEO_HEIGHT = 1080;/,
5699
+ "export const VIDEO_HEIGHT = 1920; // TikTok 9:16"
5700
+ ).replace(
5701
+ /export const VIDEO_FPS = 60;/,
5702
+ "export const VIDEO_FPS = 30; // TikTok standard"
5703
+ );
5704
+ await writeFile5(constantsPath, constantsContent, "utf-8");
5705
+ if (format === "human") {
5706
+ spinner?.stop();
5707
+ success("Configured for TikTok (1080x1920 @ 30fps)");
5708
+ spinner?.start();
5709
+ }
5710
+ } catch {
5711
+ }
5712
+ }
5718
5713
  spinner?.stop();
5719
5714
  if (format === "json") {
5720
5715
  printJson({
5721
5716
  name,
5722
5717
  path: targetDir,
5723
5718
  template: options.template,
5719
+ type: options.type,
5724
5720
  installed: options.install
5725
5721
  });
5726
5722
  return;
@@ -5731,6 +5727,11 @@ var initCommand = new Command19("init").description("Create a new Remotion video
5731
5727
  }
5732
5728
  console.log();
5733
5729
  success(`Video project "${name}" created successfully!`);
5730
+ if (options.type === "tiktok") {
5731
+ info("Format: TikTok/Reels/Shorts (1080x1920 @ 30fps)");
5732
+ } else {
5733
+ info("Format: Landscape (1920x1080 @ 60fps)");
5734
+ }
5734
5735
  console.log();
5735
5736
  info("Next steps:");
5736
5737
  info(` cd ${name}`);
@@ -5739,17 +5740,21 @@ var initCommand = new Command19("init").description("Create a new Remotion video
5739
5740
  }
5740
5741
  info(" pnpm dev # Preview in Remotion Studio");
5741
5742
  info(" cc video create ... # Generate assets to public/");
5742
- info(" pnpm render # Render final video");
5743
+ if (options.type === "tiktok") {
5744
+ info(" pnpm exec remotion render TikTokVideo # Render TikTok video");
5745
+ } else {
5746
+ info(" pnpm exec remotion render FullVideo # Render final video");
5747
+ }
5743
5748
  } catch (err) {
5744
5749
  spinner?.stop();
5745
5750
  error(err instanceof Error ? err.message : "Unknown error");
5746
5751
  process.exit(EXIT_CODES.GENERAL_ERROR);
5747
5752
  }
5748
5753
  });
5749
- var videoCommand = new Command19("video").description("Video asset generation commands").addCommand(initCommand).addCommand(createCommand2).addCommand(searchCommand2);
5754
+ var videoCommand = new Command19("video").description("Video asset generation commands").addCommand(initCommand).addCommand(createCommand3).addCommand(searchCommand2);
5750
5755
 
5751
5756
  // src/index.ts
5752
- var VERSION = "0.1.6";
5757
+ var VERSION = "0.1.7";
5753
5758
  var program = new Command20();
5754
5759
  var cmdName = brand.commands[0];
5755
5760
  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({