@conceptcraft/mindframes 0.1.10 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/postinstall.js +4 -1
- package/dist/index.js +366 -973
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -840,13 +840,13 @@ async function importPresentation(fileBuffer, fileName, options = {}) {
|
|
|
840
840
|
return result;
|
|
841
841
|
}
|
|
842
842
|
async function listBrandings() {
|
|
843
|
-
return request("/api/branding");
|
|
843
|
+
return request("/api/cli/branding");
|
|
844
844
|
}
|
|
845
845
|
async function getBranding(id) {
|
|
846
846
|
return request(`/api/branding/${id}`);
|
|
847
847
|
}
|
|
848
848
|
async function extractBranding(url, teamId) {
|
|
849
|
-
return request("/api/branding/extract", {
|
|
849
|
+
return request("/api/cli/branding/extract", {
|
|
850
850
|
method: "POST",
|
|
851
851
|
body: { url, teamId }
|
|
852
852
|
});
|
|
@@ -992,6 +992,51 @@ async function generateSpeech(ttsRequest) {
|
|
|
992
992
|
timestamps
|
|
993
993
|
};
|
|
994
994
|
}
|
|
995
|
+
async function generateSpeechBatch(batchRequest) {
|
|
996
|
+
const apiUrl = getApiUrl();
|
|
997
|
+
if (!hasAuth()) {
|
|
998
|
+
throw new ApiError(
|
|
999
|
+
`Not authenticated. Run '${brand.commands[0]} login' or set ${brand.apiKeyEnvVar} environment variable.`,
|
|
1000
|
+
401,
|
|
1001
|
+
2
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
const authHeaders = await getAuthHeaders();
|
|
1005
|
+
let response;
|
|
1006
|
+
try {
|
|
1007
|
+
response = await fetch(`${apiUrl}/api/cli/tts/batch`, {
|
|
1008
|
+
method: "POST",
|
|
1009
|
+
headers: {
|
|
1010
|
+
"Content-Type": "application/json",
|
|
1011
|
+
...authHeaders
|
|
1012
|
+
},
|
|
1013
|
+
body: JSON.stringify(batchRequest)
|
|
1014
|
+
});
|
|
1015
|
+
} catch (error2) {
|
|
1016
|
+
throw new ApiError(
|
|
1017
|
+
`Network error: ${error2 instanceof Error ? error2.message : "Unknown error"}`,
|
|
1018
|
+
0,
|
|
1019
|
+
5
|
|
1020
|
+
);
|
|
1021
|
+
}
|
|
1022
|
+
if (!response.ok) {
|
|
1023
|
+
const errorText = await response.text().catch(() => "Unknown error");
|
|
1024
|
+
let errorMessage;
|
|
1025
|
+
try {
|
|
1026
|
+
const errorJson = JSON.parse(errorText);
|
|
1027
|
+
errorMessage = errorJson.error || errorJson.message || errorText;
|
|
1028
|
+
} catch {
|
|
1029
|
+
errorMessage = errorText;
|
|
1030
|
+
}
|
|
1031
|
+
throw new ApiError(errorMessage, response.status, response.status === 401 ? 2 : 1);
|
|
1032
|
+
}
|
|
1033
|
+
const result = await response.json();
|
|
1034
|
+
result.results = result.results.map((r) => ({
|
|
1035
|
+
...r,
|
|
1036
|
+
audioData: Buffer.from(r.audioData, "base64")
|
|
1037
|
+
}));
|
|
1038
|
+
return result;
|
|
1039
|
+
}
|
|
995
1040
|
async function getVoices() {
|
|
996
1041
|
return request("/api/cli/tts");
|
|
997
1042
|
}
|
|
@@ -3019,14 +3064,14 @@ import chalk12 from "chalk";
|
|
|
3019
3064
|
|
|
3020
3065
|
// src/commands/skill/generate-main-skill.ts
|
|
3021
3066
|
function generateMainSkillContent(context) {
|
|
3022
|
-
const { name, cmd: cmd2
|
|
3023
|
-
const envPrefix = name.toUpperCase().replace(
|
|
3067
|
+
const { name, cmd: cmd2 } = context;
|
|
3068
|
+
const envPrefix = name.toUpperCase().replace(/[^A-Z0-9]/g, "_");
|
|
3024
3069
|
return `---
|
|
3025
3070
|
name: ${name}
|
|
3026
|
-
description: ${
|
|
3071
|
+
description: ${name} CLI for AI-powered content creation. Use when user needs to create presentations, generate video assets (voiceover, music, images, stock videos), use text-to-speech, mix audio, search stock media, or manage branding. This is the main entry point - load specialized skills (${name}-video, ${name}-presentation) for detailed workflows.
|
|
3027
3072
|
---
|
|
3028
3073
|
|
|
3029
|
-
# ${
|
|
3074
|
+
# ${name} CLI
|
|
3030
3075
|
|
|
3031
3076
|
A comprehensive CLI for AI-powered content creation. Generate presentations, video assets, voiceovers, music, and search stock media - all from your terminal.
|
|
3032
3077
|
|
|
@@ -3439,901 +3484,157 @@ ${cmd2} --version # Version info
|
|
|
3439
3484
|
}
|
|
3440
3485
|
|
|
3441
3486
|
// src/commands/skill/rules/video/content.ts
|
|
3442
|
-
var
|
|
3443
|
-
{
|
|
3444
|
-
filename: "video-creation-guide.md",
|
|
3445
|
-
content: `# Video Creation Guide
|
|
3446
|
-
|
|
3447
|
-
### Related Skills
|
|
3448
|
-
|
|
3449
|
-
Use these installed skills for implementation details:
|
|
3450
|
-
- \`remotion-best-practices\` \u2014 Remotion patterns and API
|
|
3451
|
-
- \`threejs-*\` skills \u2014 for R3F/WebGL (particles, 3D elements)
|
|
3452
|
-
|
|
3453
|
-
---
|
|
3454
|
-
|
|
3455
|
-
## Core Rules
|
|
3456
|
-
|
|
3457
|
-
Your task is not "making slideshows" \u2014 you are **simulating a real interface** that obeys cinematic physics.
|
|
3458
|
-
|
|
3459
|
-
### Hard Constraints
|
|
3460
|
-
|
|
3461
|
-
1. **No scene > 8 seconds** without cut or major action
|
|
3462
|
-
2. **No static pixels** \u2014 everything breathes, drifts, pulses
|
|
3463
|
-
3. **No linear interpolation** \u2014 use \`spring()\` physics
|
|
3464
|
-
4. **Scene overlap 15-20 frames** \u2014 no hard cuts
|
|
3465
|
-
5. **60 FPS mandatory** \u2014 30fps looks choppy
|
|
3466
|
-
6. **No screenshots for UI** \u2014 rebuild in React/CSS
|
|
3467
|
-
|
|
3468
|
-
---
|
|
3469
|
-
|
|
3470
|
-
## Code Organization
|
|
3471
|
-
|
|
3472
|
-
- Create separate files: \`Button.tsx\`, \`Window.tsx\`, \`Cursor.tsx\`
|
|
3473
|
-
- Use Zod schemas for props validation
|
|
3474
|
-
- Extract animation configs to constants
|
|
3475
|
-
|
|
3476
|
-
\`\`\`tsx
|
|
3477
|
-
import { spring, interpolate, useCurrentFrame, useVideoConfig } from 'remotion';
|
|
3478
|
-
|
|
3479
|
-
// ALWAYS use spring for element entrances
|
|
3480
|
-
// NEVER use magic numbers
|
|
3481
|
-
\`\`\`
|
|
3482
|
-
|
|
3483
|
-
---
|
|
3484
|
-
|
|
3485
|
-
## Aesthetics (Linear/Stripe Style)
|
|
3486
|
-
|
|
3487
|
-
\`\`\`css
|
|
3488
|
-
/* Shadows - soft, expensive */
|
|
3489
|
-
box-shadow: 0 20px 50px -12px rgba(0,0,0,0.5);
|
|
3490
|
-
|
|
3491
|
-
/* Borders - thin, barely visible */
|
|
3492
|
-
border: 1px solid rgba(255,255,255,0.1);
|
|
3493
|
-
\`\`\`
|
|
3494
|
-
|
|
3495
|
-
- Fonts: Inter or SF Pro
|
|
3496
|
-
- Never pure \`#000000\` \u2014 use \`#050505\`
|
|
3497
|
-
- Never pure \`#FFFFFF\` \u2014 use \`#F0F0F0\`
|
|
3498
|
-
|
|
3499
|
-
---
|
|
3500
|
-
|
|
3501
|
-
## Self-Check Before Render
|
|
3502
|
-
|
|
3503
|
-
- [ ] Camera rig wraps entire scene with drift/zoom
|
|
3504
|
-
- [ ] Every UI element uses 2.5D rotation entrance
|
|
3505
|
-
- [ ] Cursor moves in curves with overshoot
|
|
3506
|
-
- [ ] Lists/grids stagger (never appear all at once)
|
|
3507
|
-
- [ ] Background has moving orbs + vignette + noise
|
|
3508
|
-
- [ ] Something is moving on EVERY frame
|
|
3509
|
-
- [ ] Scene transitions overlap (no hard cuts)
|
|
3510
|
-
|
|
3511
|
-
**If your video looks like PowerPoint with voiceover \u2014 START OVER.**
|
|
3512
|
-
`
|
|
3513
|
-
},
|
|
3514
|
-
{
|
|
3515
|
-
filename: "animation-physics.md",
|
|
3516
|
-
content: `# Animation Physics
|
|
3517
|
-
|
|
3518
|
-
## Spring Configurations
|
|
3519
|
-
|
|
3520
|
-
### Heavy UI (Modals, Sidebars)
|
|
3521
|
-
\`\`\`tsx
|
|
3522
|
-
config: { mass: 1, stiffness: 100, damping: 15 }
|
|
3523
|
-
\`\`\`
|
|
3524
|
-
|
|
3525
|
-
### Light UI (Tooltips, Badges)
|
|
3526
|
-
\`\`\`tsx
|
|
3527
|
-
config: { mass: 0.6, stiffness: 180, damping: 12 }
|
|
3528
|
-
\`\`\`
|
|
3529
|
-
|
|
3530
|
-
### Standard (Snappy)
|
|
3531
|
-
\`\`\`tsx
|
|
3532
|
-
config: { mass: 1, damping: 15, stiffness: 120 }
|
|
3533
|
-
\`\`\`
|
|
3534
|
-
|
|
3535
|
-
---
|
|
3536
|
-
|
|
3537
|
-
## Staggering
|
|
3538
|
-
|
|
3539
|
-
**NEVER show a list all at once.**
|
|
3487
|
+
var THUMBNAIL_RULES = `Consider creating separate thumbnail component with Remotion (can be captured with remotion still, not used in actual video).
|
|
3540
3488
|
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
fps,
|
|
3549
|
-
config: { damping: 15, stiffness: 120 },
|
|
3550
|
-
});
|
|
3551
|
-
|
|
3552
|
-
return (
|
|
3553
|
-
<div style={{
|
|
3554
|
-
opacity: progress,
|
|
3555
|
-
transform: \`translateY(\${interpolate(progress, [0, 1], [20, 0])}px)\`,
|
|
3556
|
-
}}>
|
|
3557
|
-
{item}
|
|
3558
|
-
</div>
|
|
3559
|
-
);
|
|
3560
|
-
})}
|
|
3561
|
-
\`\`\`
|
|
3562
|
-
|
|
3563
|
-
---
|
|
3564
|
-
|
|
3565
|
-
## Cursor Movement
|
|
3566
|
-
|
|
3567
|
-
**Cursor NEVER moves in straight lines.**
|
|
3568
|
-
|
|
3569
|
-
\`\`\`tsx
|
|
3570
|
-
const progress = spring({
|
|
3571
|
-
frame: frame - startFrame,
|
|
3572
|
-
fps,
|
|
3573
|
-
config: { damping: 20, stiffness: 80 },
|
|
3574
|
-
});
|
|
3575
|
-
|
|
3576
|
-
const linearX = interpolate(progress, [0, 1], [start.x, end.x]);
|
|
3577
|
-
const linearY = interpolate(progress, [0, 1], [start.y, end.y]);
|
|
3578
|
-
|
|
3579
|
-
// THE ARC: Parabola that peaks mid-travel
|
|
3580
|
-
const arcHeight = 100;
|
|
3581
|
-
const arcOffset = Math.sin(progress * Math.PI) * arcHeight;
|
|
3582
|
-
const cursorY = linearY - arcOffset;
|
|
3583
|
-
\`\`\`
|
|
3584
|
-
|
|
3585
|
-
---
|
|
3586
|
-
|
|
3587
|
-
## Click Interaction
|
|
3588
|
-
|
|
3589
|
-
\`\`\`tsx
|
|
3590
|
-
// On click:
|
|
3591
|
-
const cursorScale = isClicking ? 0.95 : 1;
|
|
3592
|
-
const buttonScaleX = isClicking ? 1.02 : 1;
|
|
3593
|
-
const buttonScaleY = isClicking ? 0.95 : 1;
|
|
3594
|
-
// Release both with spring
|
|
3595
|
-
\`\`\`
|
|
3596
|
-
|
|
3597
|
-
---
|
|
3598
|
-
|
|
3599
|
-
## Timing Reference
|
|
3600
|
-
|
|
3601
|
-
| Action | Frames (60fps) |
|
|
3602
|
-
|--------|----------------|
|
|
3603
|
-
| Element entrance | 15-20 |
|
|
3604
|
-
| Stagger gap | 3-5 |
|
|
3605
|
-
| Hold on key info | 45-60 |
|
|
3606
|
-
| Scene transition | 20-30 |
|
|
3607
|
-
| Fast interaction | 15-20 |
|
|
3608
|
-
`
|
|
3609
|
-
},
|
|
3610
|
-
{
|
|
3611
|
-
filename: "scene-structure.md",
|
|
3612
|
-
content: `# Scene Structure
|
|
3613
|
-
|
|
3614
|
-
## SceneWrapper
|
|
3615
|
-
|
|
3616
|
-
\`\`\`tsx
|
|
3617
|
-
<SceneWrapper
|
|
3618
|
-
durationInFrames={300}
|
|
3619
|
-
transitionType="slideLeft"
|
|
3620
|
-
cameraMotion="panRight"
|
|
3621
|
-
>
|
|
3622
|
-
<FeatureLayer />
|
|
3623
|
-
<CursorLayer />
|
|
3624
|
-
<ParticleLayer />
|
|
3625
|
-
</SceneWrapper>
|
|
3626
|
-
\`\`\`
|
|
3489
|
+
**High-CTR principles:**
|
|
3490
|
+
- Expressive faces (emotion, not neutral) boost CTR 20-30%
|
|
3491
|
+
- High contrast, bold colors (yellow, orange stand out)
|
|
3492
|
+
- Simple: 3 main elements max (face + text + 1 visual)
|
|
3493
|
+
- Mobile-first: readable at 320px width (70% of views)
|
|
3494
|
+
- Minimal text: 3-5 words, bold legible fonts (60-80px)
|
|
3495
|
+
- Rule of thirds composition
|
|
3627
3496
|
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
## Layer Structure (Z-Index)
|
|
3631
|
-
|
|
3632
|
-
| Layer | Z-Index |
|
|
3633
|
-
|-------|---------|
|
|
3634
|
-
| Background orbs | 0 |
|
|
3635
|
-
| Vignette | 1 |
|
|
3636
|
-
| UI Base | 10 |
|
|
3637
|
-
| UI Elements | 20 |
|
|
3638
|
-
| Overlays | 30 |
|
|
3639
|
-
| Text/Captions | 40 |
|
|
3640
|
-
| Cursor | 50 |
|
|
3641
|
-
|
|
3642
|
-
---
|
|
3643
|
-
|
|
3644
|
-
## Case Study: SaaS Task Tracker
|
|
3645
|
-
|
|
3646
|
-
### Scene 1: "The Hook" (~5s)
|
|
3647
|
-
|
|
3648
|
-
1. Dark background (\`#0B0C10\`), grid drifting
|
|
3649
|
-
2. Scattered circles magnetically attract \u2192 morph into logo
|
|
3650
|
-
3. Logo expands \u2192 becomes sidebar navigation
|
|
3651
|
-
|
|
3652
|
-
### Scene 2: "Micro-Interaction" (~6s)
|
|
3653
|
-
|
|
3654
|
-
1. Modal "Create Issue" appears
|
|
3655
|
-
2. Text types character by character (non-uniform speed)
|
|
3656
|
-
3. \`CMD + K\` hint glows, keys animate
|
|
3657
|
-
4. Cursor flies to "Save" in arc, slows on approach
|
|
3658
|
-
|
|
3659
|
-
### Scene 3: "The Connection" (~5s)
|
|
3660
|
-
|
|
3661
|
-
1. Task card grabbed, scales 1.05, shadow deepens
|
|
3662
|
-
2. Other cards spread apart
|
|
3663
|
-
3. **Match Cut:** Zoom into avatar \u2192 color fills screen \u2192 becomes mobile notification background
|
|
3664
|
-
|
|
3665
|
-
---
|
|
3666
|
-
|
|
3667
|
-
## Composition
|
|
3668
|
-
|
|
3669
|
-
\`\`\`tsx
|
|
3670
|
-
<AbsoluteFill>
|
|
3671
|
-
<MovingBackground />
|
|
3672
|
-
<Vignette />
|
|
3673
|
-
<CameraRig>
|
|
3674
|
-
<Sequence from={0} durationInFrames={100}>
|
|
3675
|
-
<Scene1 />
|
|
3676
|
-
</Sequence>
|
|
3677
|
-
<Sequence from={85} durationInFrames={150}> {/* 15 frame overlap! */}
|
|
3678
|
-
<Scene2 />
|
|
3679
|
-
</Sequence>
|
|
3680
|
-
</CameraRig>
|
|
3681
|
-
<Audio src={music} volume={0.3} />
|
|
3682
|
-
</AbsoluteFill>
|
|
3683
|
-
\`\`\`
|
|
3684
|
-
`
|
|
3685
|
-
},
|
|
3686
|
-
{
|
|
3687
|
-
filename: "scene-transitions.md",
|
|
3688
|
-
content: `# Scene Transitions
|
|
3689
|
-
|
|
3690
|
-
**No FadeIn/FadeOut.** Only contextual transitions.
|
|
3691
|
-
|
|
3692
|
-
---
|
|
3693
|
-
|
|
3694
|
-
## Types
|
|
3695
|
-
|
|
3696
|
-
### 1. Object Persistence
|
|
3697
|
-
Same chart transforms (data, color, scale) while UI changes around it.
|
|
3698
|
-
|
|
3699
|
-
### 2. Mask Reveal
|
|
3700
|
-
Button expands to screen size via SVG \`clipPath\`.
|
|
3701
|
-
|
|
3702
|
-
### 3. Speed Ramps
|
|
3703
|
-
Scene A accelerates out, Scene B starts fast then slows.
|
|
3704
|
-
|
|
3705
|
-
---
|
|
3706
|
-
|
|
3707
|
-
## Match Cut Example
|
|
3708
|
-
|
|
3709
|
-
\`\`\`
|
|
3710
|
-
Scene A: Zoom into avatar
|
|
3711
|
-
\u2193
|
|
3712
|
-
Avatar color fills screen
|
|
3713
|
-
\u2193
|
|
3714
|
-
Scene B: That color IS the notification background
|
|
3715
|
-
\`\`\`
|
|
3716
|
-
|
|
3717
|
-
---
|
|
3718
|
-
|
|
3719
|
-
## Overlapping Sequences (CRITICAL)
|
|
3720
|
-
|
|
3721
|
-
\`\`\`tsx
|
|
3722
|
-
<Sequence from={0} durationInFrames={100}>
|
|
3723
|
-
<SceneOne />
|
|
3724
|
-
</Sequence>
|
|
3725
|
-
<Sequence from={85} durationInFrames={150}> {/* 15 frames early! */}
|
|
3726
|
-
<SceneTwo />
|
|
3727
|
-
</Sequence>
|
|
3728
|
-
\`\`\`
|
|
3729
|
-
|
|
3730
|
-
---
|
|
3731
|
-
|
|
3732
|
-
## TransitionSeries
|
|
3733
|
-
|
|
3734
|
-
\`\`\`tsx
|
|
3735
|
-
import { TransitionSeries, linearTiming } from '@remotion/transitions';
|
|
3736
|
-
import { slide } from '@remotion/transitions/slide';
|
|
3737
|
-
|
|
3738
|
-
<TransitionSeries>
|
|
3739
|
-
<TransitionSeries.Sequence durationInFrames={100}>
|
|
3740
|
-
<SceneOne />
|
|
3741
|
-
</TransitionSeries.Sequence>
|
|
3742
|
-
<TransitionSeries.Transition
|
|
3743
|
-
presentation={slide({ direction: 'from-bottom' })}
|
|
3744
|
-
timing={linearTiming({ durationInFrames: 20 })}
|
|
3745
|
-
/>
|
|
3746
|
-
<TransitionSeries.Sequence durationInFrames={150}>
|
|
3747
|
-
<SceneTwo />
|
|
3748
|
-
</TransitionSeries.Sequence>
|
|
3749
|
-
</TransitionSeries>
|
|
3750
|
-
\`\`\`
|
|
3751
|
-
`
|
|
3752
|
-
},
|
|
3753
|
-
{
|
|
3754
|
-
filename: "polish-effects.md",
|
|
3755
|
-
content: `# Polish Effects
|
|
3756
|
-
|
|
3757
|
-
## Reflection (Glass Glint)
|
|
3758
|
-
|
|
3759
|
-
Diagonal gradient sweeps every 5 seconds.
|
|
3760
|
-
|
|
3761
|
-
\`\`\`tsx
|
|
3762
|
-
const cycleFrame = frame % 300;
|
|
3763
|
-
const sweepProgress = interpolate(cycleFrame, [0, 60], [-100, 200], {
|
|
3764
|
-
extrapolateRight: 'clamp',
|
|
3765
|
-
});
|
|
3766
|
-
\`\`\`
|
|
3767
|
-
|
|
3768
|
-
---
|
|
3769
|
-
|
|
3770
|
-
## Background Breathing
|
|
3771
|
-
|
|
3772
|
-
Background is NEVER static.
|
|
3773
|
-
|
|
3774
|
-
\`\`\`tsx
|
|
3775
|
-
const orb1X = Math.sin(frame / 60) * 200;
|
|
3776
|
-
const orb1Y = Math.cos(frame / 80) * 100;
|
|
3777
|
-
\`\`\`
|
|
3778
|
-
|
|
3779
|
-
---
|
|
3780
|
-
|
|
3781
|
-
## Typewriter Effect
|
|
3782
|
-
|
|
3783
|
-
\`\`\`tsx
|
|
3784
|
-
const charIndex = Math.floor(frame / 3);
|
|
3785
|
-
const showCursor = Math.floor(frame / 15) % 2 === 0;
|
|
3786
|
-
|
|
3787
|
-
<span>
|
|
3788
|
-
{text.slice(0, charIndex)}
|
|
3789
|
-
{showCursor && <span>|</span>}
|
|
3790
|
-
</span>
|
|
3791
|
-
\`\`\`
|
|
3792
|
-
|
|
3793
|
-
---
|
|
3794
|
-
|
|
3795
|
-
## Vignette & Noise
|
|
3796
|
-
|
|
3797
|
-
\`\`\`tsx
|
|
3798
|
-
// Noise
|
|
3799
|
-
<AbsoluteFill style={{
|
|
3800
|
-
backgroundImage: 'url(/noise.png)',
|
|
3801
|
-
opacity: 0.03,
|
|
3802
|
-
mixBlendMode: 'overlay',
|
|
3803
|
-
}} />
|
|
3804
|
-
|
|
3805
|
-
// Vignette
|
|
3806
|
-
<AbsoluteFill style={{
|
|
3807
|
-
background: 'radial-gradient(ellipse at center, transparent 40%, rgba(0,0,0,0.8) 100%)',
|
|
3808
|
-
}} />
|
|
3809
|
-
\`\`\`
|
|
3810
|
-
`
|
|
3811
|
-
},
|
|
3812
|
-
{
|
|
3813
|
-
filename: "advanced-techniques.md",
|
|
3814
|
-
content: `# Advanced Techniques
|
|
3815
|
-
|
|
3816
|
-
## Audio-Reactive
|
|
3817
|
-
|
|
3818
|
-
- **Kick:** \`scale(1.005)\` pulse
|
|
3819
|
-
- **Snare:** Trigger scene changes
|
|
3820
|
-
- **Hi-hats:** Cursor flicker, particle shimmer
|
|
3821
|
-
|
|
3822
|
-
---
|
|
3823
|
-
|
|
3824
|
-
## Motion Blur (Fake)
|
|
3825
|
-
|
|
3826
|
-
\`\`\`tsx
|
|
3827
|
-
// Trail: Render 3-4 copies with opacity 0.3, 1-frame delay
|
|
3828
|
-
|
|
3829
|
-
// Or drop shadow for fast movement:
|
|
3830
|
-
filter: \`drop-shadow(\${velocityX * 0.5}px \${velocityY * 0.5}px 10px rgba(0,0,0,0.3))\`
|
|
3831
|
-
\`\`\`
|
|
3832
|
-
|
|
3833
|
-
---
|
|
3834
|
-
|
|
3835
|
-
## 3D Perspective
|
|
3836
|
-
|
|
3837
|
-
\`\`\`tsx
|
|
3838
|
-
<div style={{ perspective: '1000px' }}>
|
|
3839
|
-
<div style={{
|
|
3840
|
-
transform: 'rotateX(5deg) rotateY(10deg)',
|
|
3841
|
-
transformStyle: 'preserve-3d',
|
|
3842
|
-
}}>
|
|
3843
|
-
{/* Your UI */}
|
|
3844
|
-
</div>
|
|
3845
|
-
</div>
|
|
3846
|
-
\`\`\`
|
|
3847
|
-
|
|
3848
|
-
---
|
|
3849
|
-
|
|
3850
|
-
## Kinetic Typography
|
|
3851
|
-
|
|
3852
|
-
### Masked Reveal
|
|
3853
|
-
\`\`\`tsx
|
|
3854
|
-
<div style={{ overflow: 'hidden', height: 80 }}>
|
|
3855
|
-
<h1 style={{
|
|
3856
|
-
transform: \`translateY(\${interpolate(progress, [0, 1], [100, 0])}%)\`,
|
|
3857
|
-
}}>
|
|
3858
|
-
INTRODUCING
|
|
3859
|
-
</h1>
|
|
3860
|
-
</div>
|
|
3861
|
-
\`\`\`
|
|
3862
|
-
|
|
3863
|
-
### Keyword Animation
|
|
3864
|
-
Animate keywords, not whole sentences.
|
|
3865
|
-
`
|
|
3866
|
-
},
|
|
3867
|
-
{
|
|
3868
|
-
filename: "remotion-config.md",
|
|
3869
|
-
content: `# Remotion Configuration
|
|
3870
|
-
|
|
3871
|
-
## FPS & Resolution
|
|
3872
|
-
|
|
3873
|
-
- **60 FPS mandatory** \u2014 30fps looks choppy
|
|
3874
|
-
- **1920\xD71080** Full HD
|
|
3875
|
-
- **Center:** \`{x: 960, y: 540}\`
|
|
3876
|
-
|
|
3877
|
-
---
|
|
3878
|
-
|
|
3879
|
-
## Timing
|
|
3880
|
-
|
|
3881
|
-
- 1 second = 60 frames
|
|
3882
|
-
- Fast interaction = 15-20 frames
|
|
3883
|
-
- No scene > 8 seconds without action
|
|
3884
|
-
|
|
3885
|
-
---
|
|
3886
|
-
|
|
3887
|
-
## Entry-Action-Exit Structure
|
|
3888
|
-
|
|
3889
|
-
| Phase | Duration |
|
|
3890
|
-
|-------|----------|
|
|
3891
|
-
| Entry | 0.0s - 0.5s |
|
|
3892
|
-
| Action | 0.5s - (duration - 1s) |
|
|
3893
|
-
| Exit | last 1s |
|
|
3894
|
-
|
|
3895
|
-
---
|
|
3896
|
-
|
|
3897
|
-
## Font Loading
|
|
3898
|
-
|
|
3899
|
-
\`\`\`tsx
|
|
3900
|
-
const [handle] = useState(() => delayRender());
|
|
3901
|
-
|
|
3902
|
-
useEffect(() => {
|
|
3903
|
-
document.fonts.ready.then(() => {
|
|
3904
|
-
continueRender(handle);
|
|
3905
|
-
});
|
|
3906
|
-
}, [handle]);
|
|
3907
|
-
\`\`\`
|
|
3497
|
+
**Specs:** 1280x720, <2MB, 16:9 ratio
|
|
3908
3498
|
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
\`\`\`tsx
|
|
3914
|
-
export const SceneSchema = z.object({
|
|
3915
|
-
titleText: z.string(),
|
|
3916
|
-
buttonColor: z.string(),
|
|
3917
|
-
cursorPath: z.array(z.object({ x: z.number(), y: z.number() })),
|
|
3918
|
-
});
|
|
3919
|
-
\`\`\`
|
|
3920
|
-
|
|
3921
|
-
---
|
|
3922
|
-
|
|
3923
|
-
## SaaS Video Kit Components
|
|
3924
|
-
|
|
3925
|
-
| Component | Purpose |
|
|
3926
|
-
|-----------|---------|
|
|
3927
|
-
| \`MockWindow\` | macOS window with traffic lights |
|
|
3928
|
-
| \`SmartCursor\` | Bezier curves + click physics |
|
|
3929
|
-
| \`NotificationToast\` | Slide in, wait, slide out |
|
|
3930
|
-
| \`TypingText\` | Typewriter with cursor |
|
|
3931
|
-
| \`Placeholder\` | For logos/icons |
|
|
3932
|
-
|
|
3933
|
-
---
|
|
3934
|
-
|
|
3935
|
-
## Code Rules
|
|
3936
|
-
|
|
3937
|
-
1. No \`transition: all 0.3s\` \u2014 use \`interpolate()\` or \`spring()\`
|
|
3938
|
-
2. Use \`AbsoluteFill\` for layout
|
|
3939
|
-
3. No magic numbers \u2014 extract to constants
|
|
3940
|
-
`
|
|
3941
|
-
},
|
|
3942
|
-
{
|
|
3943
|
-
filename: "elite-production.md",
|
|
3944
|
-
content: `# Elite Production
|
|
3945
|
-
|
|
3946
|
-
For Stripe/Apple/Linear quality.
|
|
3947
|
-
|
|
3948
|
-
---
|
|
3949
|
-
|
|
3950
|
-
## Global Lighting Engine
|
|
3951
|
-
|
|
3952
|
-
\`\`\`tsx
|
|
3953
|
-
const lightSource = { x: 0.2, y: -0.5 };
|
|
3954
|
-
const gradientAngle = Math.atan2(lightSource.y, lightSource.x) * (180 / Math.PI);
|
|
3955
|
-
|
|
3956
|
-
<button style={{
|
|
3957
|
-
background: \`linear-gradient(\${gradientAngle}deg, rgba(255,255,255,0.1) 0%, transparent 50%)\`,
|
|
3958
|
-
borderTop: '1px solid rgba(255,255,255,0.15)',
|
|
3959
|
-
boxShadow: \`\${-lightSource.x * 20}px \${-lightSource.y * 20}px 40px rgba(0,0,0,0.3)\`,
|
|
3960
|
-
}} />
|
|
3961
|
-
\`\`\`
|
|
3962
|
-
|
|
3963
|
-
---
|
|
3964
|
-
|
|
3965
|
-
## Noise & Dithering
|
|
3966
|
-
|
|
3967
|
-
Every background needs noise overlay (opacity 0.02-0.05). Prevents YouTube banding.
|
|
3968
|
-
|
|
3969
|
-
---
|
|
3970
|
-
|
|
3971
|
-
## React Three Fiber
|
|
3972
|
-
|
|
3973
|
-
For particles, 3D globes \u2014 use WebGL via \`@remotion/three\`, not CSS 3D.
|
|
3974
|
-
|
|
3975
|
-
\`\`\`tsx
|
|
3976
|
-
import { ThreeCanvas } from '@remotion/three';
|
|
3977
|
-
|
|
3978
|
-
<AbsoluteFill>
|
|
3979
|
-
<HtmlUI />
|
|
3980
|
-
<ThreeCanvas>
|
|
3981
|
-
<Particles />
|
|
3982
|
-
</ThreeCanvas>
|
|
3983
|
-
</AbsoluteFill>
|
|
3984
|
-
\`\`\`
|
|
3985
|
-
|
|
3986
|
-
See \`threejs-*\` skills for implementation.
|
|
3987
|
-
|
|
3988
|
-
---
|
|
3989
|
-
|
|
3990
|
-
## Virtual Camera Rig
|
|
3991
|
-
|
|
3992
|
-
Move camera, not elements:
|
|
3993
|
-
|
|
3994
|
-
\`\`\`tsx
|
|
3995
|
-
const CameraProvider = ({ children }) => {
|
|
3996
|
-
const frame = useCurrentFrame();
|
|
3997
|
-
const panX = interpolate(frame, [0, 300], [0, -100]);
|
|
3998
|
-
const zoom = interpolate(frame, [0, 300], [1, 1.05]);
|
|
3999
|
-
|
|
4000
|
-
return (
|
|
4001
|
-
<div style={{
|
|
4002
|
-
transform: \`translateX(\${panX}px) scale(\${zoom})\`,
|
|
4003
|
-
transformOrigin: 'center',
|
|
4004
|
-
}}>
|
|
4005
|
-
{children}
|
|
4006
|
-
</div>
|
|
4007
|
-
);
|
|
4008
|
-
};
|
|
4009
|
-
\`\`\`
|
|
4010
|
-
|
|
4011
|
-
---
|
|
4012
|
-
|
|
4013
|
-
## Motion Rules
|
|
4014
|
-
|
|
4015
|
-
- **Overshoot:** Modal scales to 1.02, settles to 1.0
|
|
4016
|
-
- **Overlap:** Scene B starts 15 frames before Scene A ends
|
|
4017
|
-
`
|
|
4018
|
-
},
|
|
4019
|
-
{
|
|
4020
|
-
filename: "known-issues.md",
|
|
4021
|
-
content: `# Known Issues & Fixes
|
|
4022
|
-
|
|
4023
|
-
## 1. Music Ends Before Video Finishes
|
|
4024
|
-
|
|
4025
|
-
**Problem:** Music duration is shorter than video duration, causing awkward silence at the end.
|
|
4026
|
-
|
|
4027
|
-
**Solution:** Loop music in Remotion using the \`loop\` prop:
|
|
3499
|
+
Can capture: \`pnpm exec remotion still ThumbnailScene out/thumb.png\`
|
|
3500
|
+
`;
|
|
3501
|
+
var MOTION_DESIGN_GUIDELINES = `# Motion Design Principles
|
|
4028
3502
|
|
|
4029
|
-
|
|
4030
|
-
import { Audio } from 'remotion';
|
|
3503
|
+
**Core Philosophy:** "Atomic, Kinetic Construction" - nothing is static. Elements arrive and leave via physics-based transitions.
|
|
4031
3504
|
|
|
4032
|
-
|
|
4033
|
-
\`\`\`
|
|
3505
|
+
## Design System Approach
|
|
4034
3506
|
|
|
4035
|
-
**
|
|
4036
|
-
-
|
|
4037
|
-
-
|
|
4038
|
-
-
|
|
3507
|
+
**Separate content from logic:**
|
|
3508
|
+
- Theme object: colors (primary, accent, background, text), fonts, corner radiuses in config
|
|
3509
|
+
- Scene object: define by duration in frames and content type, not timecodes
|
|
3510
|
+
- Avoid hardcoding: colors, text, data values can be passed via props or config file
|
|
4039
3511
|
|
|
4040
|
-
|
|
3512
|
+
## Animation Physics (Spring-Based)
|
|
4041
3513
|
|
|
4042
|
-
|
|
3514
|
+
**Spring Pop:**
|
|
3515
|
+
- UI cards, bubbles, logos "pop" with bounce
|
|
3516
|
+
- \`spring()\` function works well: low mass (0.5), moderate damping (10-12), high stiffness (100-200)
|
|
3517
|
+
- Spring value can map to scale (0 to 1) and opacity (0 to 1)
|
|
3518
|
+
- Consider \`transform-origin\` placement (center for bubbles, top for dropdowns)
|
|
4043
3519
|
|
|
4044
|
-
**
|
|
3520
|
+
**Kinetic Typography:**
|
|
3521
|
+
- Text entering line-by-line or word-by-word (not all at once)
|
|
3522
|
+
- Can split text into arrays, stagger with delays (index * 5 frames)
|
|
3523
|
+
- \`interpolate()\` works for opacity [0,1] and translateY [20px, 0px] - slide up
|
|
3524
|
+
- Cubic easing works well for slide-up motion
|
|
4045
3525
|
|
|
4046
|
-
**
|
|
4047
|
-
|
|
4048
|
-
|
|
3526
|
+
**Constructed UI:**
|
|
3527
|
+
- Building UI from HTML/CSS divs works better than screenshots
|
|
3528
|
+
- If user shares project: study actual UI components (buttons, cards, modals) and implement pixel-perfect recreations - match colors, fonts, shadows, border-radius
|
|
3529
|
+
- Bar charts: can animate height/width from 0% to target
|
|
3530
|
+
- Line charts: can animate SVG path \`stroke-dashoffset\`
|
|
3531
|
+
- Donut charts: can animate \`stroke-dasharray\` of SVG circle
|
|
3532
|
+
- Numbers: counter component interpolating from 0 to target over 30-60 frames
|
|
4049
3533
|
|
|
4050
|
-
|
|
4051
|
-
const musicVolume = interpolate(
|
|
4052
|
-
frame,
|
|
4053
|
-
[0, 30, totalFrames - 60, totalFrames],
|
|
4054
|
-
[0, 0.3, 0.3, 0],
|
|
4055
|
-
{ extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }
|
|
4056
|
-
);
|
|
3534
|
+
## Visual Composition
|
|
4057
3535
|
|
|
4058
|
-
|
|
4059
|
-
|
|
3536
|
+
**Background Ambience:**
|
|
3537
|
+
- Static backgrounds feel flat - consider faint dots/patterns
|
|
3538
|
+
- Slow oscillation works well: \`Math.sin(frame / 100)\` applied to position/rotation for "floating" effect
|
|
3539
|
+
- Parallax adds depth: background moves slower than foreground
|
|
4060
3540
|
|
|
4061
|
-
|
|
3541
|
+
**SVG Handling:**
|
|
3542
|
+
- Inline SVGs allow control of fill color via theme (better than img tags)
|
|
3543
|
+
- Chat bubbles can be constructed with SVG paths or heavy border-radius
|
|
3544
|
+
- Animating bubble "tail" separately adds polish
|
|
4062
3545
|
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
**Fix:** Use \`@remotion/transitions\` with overlapping:
|
|
4068
|
-
\`\`\`tsx
|
|
4069
|
-
import { TransitionSeries, springTiming } from '@remotion/transitions';
|
|
4070
|
-
import { slide } from '@remotion/transitions/slide';
|
|
4071
|
-
import { fade } from '@remotion/transitions/fade';
|
|
4072
|
-
|
|
4073
|
-
<TransitionSeries>
|
|
4074
|
-
<TransitionSeries.Sequence durationInFrames={sceneA.frames}>
|
|
4075
|
-
<SceneA />
|
|
4076
|
-
</TransitionSeries.Sequence>
|
|
4077
|
-
<TransitionSeries.Transition
|
|
4078
|
-
presentation={slide({ direction: 'from-right' })}
|
|
4079
|
-
timing={springTiming({ config: { damping: 20 } })}
|
|
4080
|
-
/>
|
|
4081
|
-
<TransitionSeries.Sequence durationInFrames={sceneB.frames}>
|
|
4082
|
-
<SceneB />
|
|
4083
|
-
</TransitionSeries.Sequence>
|
|
4084
|
-
</TransitionSeries>
|
|
4085
|
-
\`\`\`
|
|
4086
|
-
|
|
4087
|
-
---
|
|
4088
|
-
|
|
4089
|
-
## 4. Voiceover Lacks Energy
|
|
4090
|
-
|
|
4091
|
-
**Problem:** Voiceover sounds flat/monotone.
|
|
4092
|
-
|
|
4093
|
-
**Fix:** Pass \`voiceSettings\` in scenes JSON:
|
|
4094
|
-
\`\`\`json
|
|
4095
|
-
{
|
|
4096
|
-
"scenes": [...],
|
|
4097
|
-
"voice": "Kore",
|
|
4098
|
-
"voiceSettings": {
|
|
4099
|
-
"style": 0.6,
|
|
4100
|
-
"stability": 0.4,
|
|
4101
|
-
"speed": 0.95
|
|
4102
|
-
}
|
|
4103
|
-
}
|
|
4104
|
-
\`\`\`
|
|
3546
|
+
**Scene Transitions:**
|
|
3547
|
+
- Scenes can slide or camera can "pan" to new area
|
|
3548
|
+
- Slide-out approach: Scene A \`translateX\` 0% to -100%, Scene B 100% to 0%
|
|
3549
|
+
- Spatial pan approach: place scenes on giant canvas, animate parent container transform
|
|
4105
3550
|
|
|
4106
|
-
|
|
4107
|
-
- \`stability\`: 0.3-0.5 for more variation
|
|
4108
|
-
- \`speed\`: 0.9-1.0 slightly slower = more impactful
|
|
3551
|
+
## Suggested Component Architecture
|
|
4109
3552
|
|
|
4110
|
-
|
|
3553
|
+
Consider these reusable patterns:
|
|
3554
|
+
- KineticText: text, delay, style props - handles word-splitting and stagger
|
|
3555
|
+
- SmartCard: container with Spring Pop entry and glassmorphism styles
|
|
3556
|
+
- AnimatedCounter: from, to, duration props - number ticking
|
|
3557
|
+
- ProgressBar/ChartElement: percentage, color props - growth animation from 0
|
|
4111
3558
|
|
|
4112
|
-
|
|
3559
|
+
**Motion Blur (optional):** Can simulate by stretching element in direction of movement on fast transitions.
|
|
3560
|
+
`;
|
|
3561
|
+
var SVG_ANIMATION_GUIDELINES = `# SVG Line Animation (Write-On Effect)
|
|
4113
3562
|
|
|
4114
|
-
**
|
|
3563
|
+
**Core Concept:** "Invisible Ink Rule" - lines draw in (don't fade in), as if hand-drawn in real-time.
|
|
4115
3564
|
|
|
4116
|
-
**
|
|
4117
|
-
1
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
const paddedDuration = voiceoverFrames + 30;
|
|
4122
|
-
\`\`\`
|
|
4123
|
-
3. **Brief should note:** "Duration based on voiceover length"
|
|
3565
|
+
**Animation approach:**
|
|
3566
|
+
- Setting \`pathLength="1"\` on SVG path elements normalizes length
|
|
3567
|
+
- Animating \`strokeDashoffset\` from 1 (hidden) to 0 (drawn) creates write-on effect
|
|
3568
|
+
- \`strokeDasharray: 1\` with interpolate \`[1, 0]\` over 20-30 frames works well
|
|
3569
|
+
- \`stroke-linecap="round"\` creates friendly hand-drawn look
|
|
4124
3570
|
|
|
4125
|
-
|
|
3571
|
+
**Draw & vanish sequence:**
|
|
3572
|
+
- Draw in: 20 frames (offset 1\u21920)
|
|
3573
|
+
- Hold: 10 frames
|
|
3574
|
+
- Draw out: fade opacity or continue offset
|
|
4126
3575
|
|
|
4127
|
-
|
|
3576
|
+
**Reusable component pattern:**
|
|
3577
|
+
- Props: path data (d), color, width, delay, type (underline/spark/circle/arrow)
|
|
3578
|
+
- Pre-defined path dictionaries work better than generating random coordinates
|
|
3579
|
+
- Positioning with top/left/scale/rotation props for text accents
|
|
3580
|
+
`;
|
|
3581
|
+
var ASSET_USAGE_GUIDELINES = `# Asset Usage & Optimization
|
|
4128
3582
|
|
|
4129
|
-
**
|
|
3583
|
+
**For project-specific videos:** Study the project first - extract logos, colors, fonts, actual UI components. Recreate components pixel-perfect in Remotion (match exact colors, shadows, border-radius, fonts). Use project's actual branding and design system for authentic look.
|
|
4130
3584
|
|
|
4131
|
-
**
|
|
4132
|
-
1. Find project's actual components (buttons, cards, modals, inputs)
|
|
4133
|
-
2. Copy their styles/structure into Remotion components
|
|
4134
|
-
3. Match colors, fonts, shadows, border-radius exactly
|
|
3585
|
+
**CLI provides flexible asset search** - images and videos can be used creatively throughout compositions.
|
|
4135
3586
|
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
3587
|
+
**Video assets (from CLI video search):**
|
|
3588
|
+
- Full-screen backgrounds (with overlays/text)
|
|
3589
|
+
- Embedded in UI cards or windows alongside text
|
|
3590
|
+
- Picture-in-picture style elements
|
|
3591
|
+
- Background layers with reduced opacity
|
|
3592
|
+
- Transitional footage between scenes
|
|
4139
3593
|
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
boxShadow: '0 4px 14px rgba(99, 102, 241, 0.4)',
|
|
4146
|
-
border: '1px solid rgba(255,255,255,0.1)',
|
|
4147
|
-
}}>Click</button>
|
|
4148
|
-
\`\`\`
|
|
3594
|
+
**Image assets (from CLI image search):**
|
|
3595
|
+
- Scene backgrounds (static or with animation)
|
|
3596
|
+
- Embedded elements within compositions
|
|
3597
|
+
- UI component content (cards, panels)
|
|
3598
|
+
- Layered for depth and parallax effects
|
|
4149
3599
|
|
|
4150
|
-
|
|
3600
|
+
**Dynamic backgrounds (Three.js/WebGL):**
|
|
3601
|
+
- Three.js/React Three Fiber for performance-optimized animated backgrounds
|
|
3602
|
+
- Particle systems, procedural gradients, geometric patterns
|
|
3603
|
+
- SVG animations for abstract shapes and patterns
|
|
3604
|
+
- WebGL shaders for dynamic effects
|
|
3605
|
+
- Combines well with static assets for depth
|
|
4151
3606
|
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
**Problem:** Video feels flat, no depth or motion.
|
|
4155
|
-
|
|
4156
|
-
**Checklist:**
|
|
4157
|
-
- [ ] Global light source defined (affects all shadows/gradients)
|
|
4158
|
-
- [ ] Camera rig with subtle drift/zoom
|
|
4159
|
-
- [ ] Spring physics on ALL entrances (no linear)
|
|
4160
|
-
- [ ] Staggered animations (never all at once)
|
|
4161
|
-
- [ ] Background orbs/particles moving
|
|
4162
|
-
- [ ] Noise overlay (opacity 0.02-0.05)
|
|
4163
|
-
- [ ] Vignette for depth
|
|
4164
|
-
`
|
|
4165
|
-
}
|
|
4166
|
-
];
|
|
3607
|
+
**Best approach:** Mix CLI assets (images/videos) with generated elements (Three.js, SVG) for rich, performant compositions.
|
|
3608
|
+
`;
|
|
4167
3609
|
|
|
4168
3610
|
// src/commands/skill/generate-video-skill.ts
|
|
4169
3611
|
function generateVideoSkillContent(context) {
|
|
4170
|
-
const { name, cmd: cmd2
|
|
3612
|
+
const { name, cmd: cmd2 } = context;
|
|
4171
3613
|
return `---
|
|
4172
3614
|
name: ${name}-video
|
|
4173
3615
|
description: Use when user asks to create videos (product demos, explainers, social content, promos). Handles video asset generation, Remotion implementation, and thumbnail embedding.
|
|
4174
3616
|
---
|
|
4175
3617
|
|
|
4176
|
-
# ${
|
|
4177
|
-
|
|
4178
|
-
Create professional product videos directly from your terminal. The CLI generates AI-powered video assets (voiceover, music, images, stock videos) and provides tools for Remotion-based video production with React Three Fiber.
|
|
4179
|
-
|
|
4180
|
-
**Stack:** Remotion (React video framework) + React Three Fiber (R3F) + Three.js for 3D/WebGL, particles, shaders, lighting.
|
|
4181
|
-
|
|
4182
|
-
We create **elite product videos** (Stripe, Apple, Linear quality) using physics-based animation, dynamic lighting, and pixel-perfect UI components rebuilt from the real project \u2014 never boring screenshots or static images.
|
|
4183
|
-
|
|
4184
|
-
**Core Philosophy:** "Nothing sits still. Everything is physics-based. Every pixel breathes."
|
|
4185
|
-
|
|
4186
|
-
---
|
|
4187
|
-
|
|
4188
|
-
## CRITICAL: Professional Composition Rules
|
|
4189
|
-
|
|
4190
|
-
**These rules are MANDATORY for all marketing/product videos:**
|
|
4191
|
-
|
|
4192
|
-
### \u274C NEVER DO:
|
|
4193
|
-
1. **Walls of text** - No dense paragraphs or lists longer than 3 lines
|
|
4194
|
-
2. **Flying/floating cards** - No random floating animations across the screen
|
|
4195
|
-
3. **Stretched layouts** - No elements awkwardly stretched to fill space
|
|
4196
|
-
4. **Truncated text** - Never show "Text that gets cut off..."
|
|
4197
|
-
5. **Information overload** - Max 1-2 key points visible at once
|
|
4198
|
-
6. **Amateur motion** - No PowerPoint-style "fly in from left/right"
|
|
4199
|
-
|
|
4200
|
-
### \u2705 ALWAYS DO:
|
|
4201
|
-
1. **Hierarchy first** - One clear focal point per scene (headline OR stat OR visual, not all)
|
|
4202
|
-
2. **Breathing room** - Generous whitespace (min 100px padding around elements)
|
|
4203
|
-
3. **Purposeful motion** - Cards appear with subtle spring (0-20px translateY), not fly across screen
|
|
4204
|
-
4. **Readable text** - Max 2-3 lines per card, 24px+ font size
|
|
4205
|
-
5. **Grid alignment** - Use invisible grid (3-column or 4-column layout)
|
|
4206
|
-
6. **Professional entrance** - Elements fade + slight translate (15px max), hold for 2-3s, then exit
|
|
4207
|
-
|
|
4208
|
-
### Composition Examples:
|
|
4209
|
-
|
|
4210
|
-
**\u274C BAD - Wall of Text:**
|
|
4211
|
-
\`\`\`tsx
|
|
4212
|
-
// DON'T: 10 bullet points crammed in a card
|
|
4213
|
-
<Card>
|
|
4214
|
-
<ul>
|
|
4215
|
-
{[...10items].map(item => <li>{item.longText}...</li>)}
|
|
4216
|
-
</ul>
|
|
4217
|
-
</Card>
|
|
4218
|
-
\`\`\`
|
|
4219
|
-
|
|
4220
|
-
**\u2705 GOOD - Single Focus:**
|
|
4221
|
-
\`\`\`tsx
|
|
4222
|
-
// DO: One headline, one supporting stat
|
|
4223
|
-
<AbsoluteFill style={{ alignItems: 'center', justifyContent: 'center' }}>
|
|
4224
|
-
<h1 style={{ fontSize: 72, marginBottom: 40 }}>12 hours wasted</h1>
|
|
4225
|
-
<p style={{ fontSize: 28, opacity: 0.7 }}>per week on manual tasks</p>
|
|
4226
|
-
</AbsoluteFill>
|
|
4227
|
-
\`\`\`
|
|
4228
|
-
|
|
4229
|
-
**\u274C BAD - Flying Cards:**
|
|
4230
|
-
\`\`\`tsx
|
|
4231
|
-
// DON'T: Cards flying from random positions
|
|
4232
|
-
<Card style={{
|
|
4233
|
-
transform: \`translateX(\${interpolate(progress, [0,1], [-500, 0])}px)\` // Flies from left
|
|
4234
|
-
}} />
|
|
4235
|
-
\`\`\`
|
|
4236
|
-
|
|
4237
|
-
**\u2705 GOOD - Subtle Entrance:**
|
|
4238
|
-
\`\`\`tsx
|
|
4239
|
-
// DO: Gentle spring entrance with minimal movement
|
|
4240
|
-
const progress = spring({ frame: frame - startFrame, fps, config: { damping: 20, stiffness: 100 }});
|
|
4241
|
-
<Card style={{
|
|
4242
|
-
opacity: progress,
|
|
4243
|
-
transform: \`translateY(\${interpolate(progress, [0,1], [15, 0])}px)\` // Subtle 15px drop
|
|
4244
|
-
}} />
|
|
4245
|
-
\`\`\`
|
|
4246
|
-
|
|
4247
|
-
### Layout Grid System:
|
|
4248
|
-
|
|
4249
|
-
**Use 12-column grid (like Bootstrap):**
|
|
4250
|
-
\`\`\`tsx
|
|
4251
|
-
const GRID = {
|
|
4252
|
-
columns: 12,
|
|
4253
|
-
gutter: 40,
|
|
4254
|
-
padding: 120 // Edge padding
|
|
4255
|
-
};
|
|
4256
|
-
|
|
4257
|
-
// Center 6 columns for main content
|
|
4258
|
-
const contentWidth = (1920 - (GRID.padding * 2) - (GRID.gutter * 5)) / 2;
|
|
4259
|
-
\`\`\`
|
|
3618
|
+
# ${name} Video Creation CLI
|
|
4260
3619
|
|
|
4261
|
-
|
|
4262
|
-
- **Top-left:** Brand logo, context (10% from edges)
|
|
4263
|
-
- **Center:** Primary headline/stat/demo (50% transform)
|
|
4264
|
-
- **Bottom:** CTA or tagline (10% from bottom)
|
|
4265
|
-
- **Never:** Random floating between these zones
|
|
3620
|
+
Generate video assets (voiceover, music, images, stock videos) and render with Remotion.
|
|
4266
3621
|
|
|
4267
3622
|
---
|
|
4268
3623
|
|
|
4269
3624
|
## Prerequisites
|
|
4270
3625
|
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
1. **Load related skills:**
|
|
4274
|
-
\`\`\`
|
|
4275
|
-
remotion-best-practices
|
|
4276
|
-
threejs-fundamentals
|
|
4277
|
-
\`\`\`
|
|
4278
|
-
|
|
4279
|
-
2. **Authenticate:**
|
|
4280
|
-
\`\`\`bash
|
|
4281
|
-
${cmd2} login
|
|
4282
|
-
\`\`\`
|
|
4283
|
-
|
|
4284
|
-
3. **Remotion installed** (if creating videos):
|
|
4285
|
-
\`\`\`bash
|
|
4286
|
-
pnpm install remotion @remotion/cli
|
|
4287
|
-
\`\`\`
|
|
4288
|
-
|
|
4289
|
-
---
|
|
4290
|
-
|
|
4291
|
-
## Video Creation Workflow
|
|
4292
|
-
|
|
4293
|
-
### Phase 0: Load Skills (MANDATORY)
|
|
4294
|
-
|
|
4295
|
-
Before ANY video work, invoke these skills:
|
|
4296
|
-
\`\`\`
|
|
4297
|
-
remotion-best-practices
|
|
4298
|
-
threejs-fundamentals
|
|
4299
|
-
\`\`\`
|
|
4300
|
-
|
|
4301
|
-
### Phase 1: Discovery
|
|
4302
|
-
|
|
4303
|
-
Explore current directory silently:
|
|
4304
|
-
- Understand what the product does (README, docs, code)
|
|
4305
|
-
- Find branding: logo, colors, fonts
|
|
4306
|
-
- Find UI components to copy into Remotion (buttons, cards, modals, etc.) \u2014 rebuild pixel-perfect, no screenshots
|
|
4307
|
-
|
|
4308
|
-
### Phase 2: Video Brief
|
|
4309
|
-
|
|
4310
|
-
Present a brief outline (scenes \u22648s each, duration, assets found) and get user approval before production.
|
|
4311
|
-
|
|
4312
|
-
### Phase 3: Production
|
|
4313
|
-
|
|
4314
|
-
1. **Generate audio assets** - \`${cmd2} video create\` with scenes JSON
|
|
4315
|
-
- IMPORTANT: Music is generated LAST after all voiceover/audio to ensure exact duration match
|
|
4316
|
-
2. **Scaffold OUTSIDE project** - \`cd .. && ${cmd2} video init my-video\`
|
|
4317
|
-
3. **Copy assets + UI components** from project into video project
|
|
4318
|
-
4. **Implement** - follow rules below
|
|
4319
|
-
|
|
4320
|
-
### Phase 4: Render & Thumbnail (REQUIRED)
|
|
4321
|
-
|
|
3626
|
+
**Authenticate:**
|
|
4322
3627
|
\`\`\`bash
|
|
4323
|
-
|
|
4324
|
-
pnpm exec remotion render FullVideo
|
|
4325
|
-
|
|
4326
|
-
# 2. ALWAYS embed thumbnail before delivering
|
|
4327
|
-
${cmd2} video thumbnail out/FullVideo.mp4 --frame 60
|
|
3628
|
+
${cmd2} login
|
|
4328
3629
|
\`\`\`
|
|
4329
3630
|
|
|
4330
|
-
**Note:** Remotion videos include per-scene voiceovers and background music baked in during render.
|
|
4331
|
-
|
|
4332
3631
|
---
|
|
4333
3632
|
|
|
4334
|
-
##
|
|
3633
|
+
## Video Creation Workflow
|
|
3634
|
+
|
|
3635
|
+
### 1. Generate Assets
|
|
4335
3636
|
|
|
4336
|
-
Generate voiceover, music, and visual assets
|
|
3637
|
+
Generate voiceover, music, and visual assets:
|
|
4337
3638
|
|
|
4338
3639
|
\`\`\`bash
|
|
4339
3640
|
cat <<SCENES | ${cmd2} video create --output ./public
|
|
@@ -4342,99 +3643,79 @@ cat <<SCENES | ${cmd2} video create --output ./public
|
|
|
4342
3643
|
{
|
|
4343
3644
|
"name": "Hook",
|
|
4344
3645
|
"script": "Watch how we transformed this complex workflow into a single click.",
|
|
4345
|
-
"imageQuery": "modern dashboard interface dark theme"
|
|
4346
|
-
"videoQuery": "abstract tech particles animation"
|
|
3646
|
+
"imageQuery": "modern dashboard interface dark theme"
|
|
4347
3647
|
},
|
|
4348
3648
|
{
|
|
4349
3649
|
"name": "Demo",
|
|
4350
|
-
"script": "Our AI analyzes your data in real-time, surfacing insights that matter."
|
|
4351
|
-
"imageQuery": "data visualization charts analytics"
|
|
4352
|
-
},
|
|
4353
|
-
{
|
|
4354
|
-
"name": "CTA",
|
|
4355
|
-
"script": "Start your free trial today. No credit card required.",
|
|
4356
|
-
"imageQuery": "call to action button modern"
|
|
3650
|
+
"script": "Our AI analyzes your data in real-time, surfacing insights that matter."
|
|
4357
3651
|
}
|
|
4358
3652
|
],
|
|
4359
|
-
"voice": "Kore"
|
|
4360
|
-
"voiceSettings": {
|
|
4361
|
-
"style": 0.6,
|
|
4362
|
-
"stability": 0.4,
|
|
4363
|
-
"speed": 0.95
|
|
4364
|
-
},
|
|
4365
|
-
"musicPrompt": "upbeat corporate, positive energy, modern synth"
|
|
3653
|
+
"voice": "Kore"
|
|
4366
3654
|
}
|
|
4367
3655
|
SCENES
|
|
4368
3656
|
\`\`\`
|
|
4369
3657
|
|
|
4370
3658
|
**Output:**
|
|
4371
|
-
- \`public/audio
|
|
4372
|
-
- \`public/audio/music.mp3\` - background music
|
|
4373
|
-
- \`public/video-manifest.json\` - timing
|
|
4374
|
-
- Stock images/videos (if requested)
|
|
3659
|
+
- \`public/audio/*.wav\` - scene voiceovers
|
|
3660
|
+
- \`public/audio/music.mp3\` - background music
|
|
3661
|
+
- \`public/video-manifest.json\` - timing, metadata, timeline
|
|
4375
3662
|
|
|
4376
|
-
|
|
3663
|
+
### 2. Initialize Remotion (MANDATORY)
|
|
4377
3664
|
|
|
4378
|
-
|
|
3665
|
+
Scaffold the template and copy assets:
|
|
4379
3666
|
|
|
4380
|
-
|
|
3667
|
+
\`\`\`bash
|
|
3668
|
+
cd .. && ${cmd2} video init my-video
|
|
3669
|
+
cd my-video
|
|
3670
|
+
# Assets are now in public/ directory
|
|
3671
|
+
\`\`\`
|
|
4381
3672
|
|
|
4382
|
-
|
|
3673
|
+
### 3. Render Video
|
|
4383
3674
|
|
|
3675
|
+
**For landscape videos (16:9):**
|
|
4384
3676
|
\`\`\`bash
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
cat scenes.json | ${cmd2} video create --output ./public
|
|
3677
|
+
pnpm exec remotion render FullVideo out/video.mp4
|
|
3678
|
+
\`\`\`
|
|
4388
3679
|
|
|
4389
|
-
|
|
4390
|
-
|
|
3680
|
+
**For vertical videos (9:16) with captions:**
|
|
3681
|
+
\`\`\`bash
|
|
3682
|
+
pnpm exec remotion render CaptionedVideo out/tiktok.mp4 \\
|
|
3683
|
+
--props='{"timeline":'$(cat public/video-manifest.json | jq -c .timeline)',"showCaptions":true}'
|
|
3684
|
+
\`\`\`
|
|
4391
3685
|
|
|
4392
|
-
|
|
4393
|
-
${cmd2} video thumbnail out/video.mp4 --frame 60
|
|
3686
|
+
### 4. Generate & Inject Thumbnail
|
|
4394
3687
|
|
|
4395
|
-
|
|
4396
|
-
${cmd2} images search "mountain landscape" --limit 10
|
|
4397
|
-
${cmd2} videos search "ocean waves" --limit 5
|
|
3688
|
+
${THUMBNAIL_RULES}
|
|
4398
3689
|
|
|
4399
|
-
|
|
4400
|
-
${cmd2}
|
|
4401
|
-
${cmd2} music generate "upbeat corporate" --duration 30
|
|
3690
|
+
\`\`\`bash
|
|
3691
|
+
${cmd2} video thumbnail out/video.mp4 --image out/thumb.png
|
|
4402
3692
|
\`\`\`
|
|
4403
3693
|
|
|
4404
3694
|
---
|
|
4405
3695
|
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
1. **Keep scenes under 8 seconds** without cuts or major action
|
|
4409
|
-
2. **Use spring physics** for all animations, never linear
|
|
4410
|
-
3. **Rebuild UI components** in React/CSS, no screenshots
|
|
4411
|
-
4. **Test with thumbnail embedding** before delivering
|
|
4412
|
-
5. **Music volume at 30%** (30-40% less loud than voice)
|
|
4413
|
-
6. **Read all video rules** in Phase 0 before implementation
|
|
3696
|
+
${MOTION_DESIGN_GUIDELINES}
|
|
4414
3697
|
|
|
4415
3698
|
---
|
|
4416
3699
|
|
|
4417
|
-
|
|
3700
|
+
${SVG_ANIMATION_GUIDELINES}
|
|
4418
3701
|
|
|
4419
|
-
|
|
4420
|
-
- Check authentication: \`${cmd2} whoami\`
|
|
4421
|
-
- Verify asset generation: check \`video-manifest.json\`
|
|
4422
|
-
- Voiceover flat: increase style (0.5-0.7), decrease stability (0.3-0.5)
|
|
4423
|
-
- Duration mismatch: adjust \`voiceSettings.speed\` or add padding in Remotion
|
|
3702
|
+
---
|
|
4424
3703
|
|
|
4425
|
-
|
|
3704
|
+
${ASSET_USAGE_GUIDELINES}
|
|
3705
|
+
|
|
3706
|
+
---
|
|
4426
3707
|
`;
|
|
4427
3708
|
}
|
|
4428
3709
|
|
|
4429
3710
|
// src/commands/skill/generate-presentation-skill.ts
|
|
4430
3711
|
function generatePresentationSkillContent(context) {
|
|
4431
|
-
const { name, cmd: cmd2
|
|
3712
|
+
const { name, cmd: cmd2 } = context;
|
|
4432
3713
|
return `---
|
|
4433
3714
|
name: ${name}-presentation
|
|
4434
3715
|
description: Use when user asks to create presentations (slides, decks, pitch decks). Generates AI-powered presentations with structured content and professional design.
|
|
4435
3716
|
---
|
|
4436
3717
|
|
|
4437
|
-
# ${
|
|
3718
|
+
# ${name} Presentation Creation CLI
|
|
4438
3719
|
|
|
4439
3720
|
Create professional presentations (slides, decks, pitch decks) directly from your terminal. The CLI generates AI-powered slides from any context you provide - text, files, URLs, or piped content. Also supports searching for stock images and videos.
|
|
4440
3721
|
|
|
@@ -4701,9 +3982,10 @@ function getSupportedEditorNames() {
|
|
|
4701
3982
|
// src/commands/skill/index.ts
|
|
4702
3983
|
var SKILL_TYPES = ["main", "video", "presentation"];
|
|
4703
3984
|
var skillContext = {
|
|
4704
|
-
name: brand.name,
|
|
4705
3985
|
cmd: brand.commands[0],
|
|
4706
|
-
|
|
3986
|
+
pkg: brand.packageName,
|
|
3987
|
+
url: brand.apiUrl,
|
|
3988
|
+
name: brand.displayName
|
|
4707
3989
|
};
|
|
4708
3990
|
var skillCommand = new Command14("skill").description(`Manage ${brand.displayName} skills for AI coding assistants`).addHelpText(
|
|
4709
3991
|
"after",
|
|
@@ -5288,6 +4570,13 @@ function calculateSectionTiming(sections, totalDuration, fps = DEFAULT_FPS, time
|
|
|
5288
4570
|
const proportion = wordCount / totalWords;
|
|
5289
4571
|
const durationInSeconds = totalDuration * proportion;
|
|
5290
4572
|
const durationInFrames = Math.round(durationInSeconds * fps);
|
|
4573
|
+
const chars = text.split("");
|
|
4574
|
+
const charDuration = durationInSeconds / chars.length;
|
|
4575
|
+
const approximateTimestamps = {
|
|
4576
|
+
characters: chars,
|
|
4577
|
+
characterStartTimesSeconds: chars.map((_, i) => i * charDuration),
|
|
4578
|
+
characterEndTimesSeconds: chars.map((_, i) => (i + 1) * charDuration)
|
|
4579
|
+
};
|
|
5291
4580
|
const section = {
|
|
5292
4581
|
id: index + 1,
|
|
5293
4582
|
text,
|
|
@@ -5295,7 +4584,9 @@ function calculateSectionTiming(sections, totalDuration, fps = DEFAULT_FPS, time
|
|
|
5295
4584
|
startTime: currentTime,
|
|
5296
4585
|
endTime: currentTime + durationInSeconds,
|
|
5297
4586
|
durationInSeconds,
|
|
5298
|
-
durationInFrames
|
|
4587
|
+
durationInFrames,
|
|
4588
|
+
timestamps: approximateTimestamps
|
|
4589
|
+
// Always include timestamps (approximate if needed)
|
|
5299
4590
|
};
|
|
5300
4591
|
currentTime += durationInSeconds;
|
|
5301
4592
|
return section;
|
|
@@ -5322,6 +4613,13 @@ function calculateSectionTimingFromTimestamps(sections, timestamps, fps) {
|
|
|
5322
4613
|
const endTime = characterEndTimesSeconds[Math.min(endCharIndex, characterEndTimesSeconds.length - 1)] || startTime + 1;
|
|
5323
4614
|
const durationInSeconds = endTime - startTime;
|
|
5324
4615
|
const durationInFrames = Math.round(durationInSeconds * fps);
|
|
4616
|
+
const sectionTimestamps = {
|
|
4617
|
+
characters: characters.slice(startCharIndex, endCharIndex + 1),
|
|
4618
|
+
characterStartTimesSeconds: characterStartTimesSeconds.slice(startCharIndex, endCharIndex + 1).map((t) => t - startTime),
|
|
4619
|
+
// Make relative to section start
|
|
4620
|
+
characterEndTimesSeconds: characterEndTimesSeconds.slice(startCharIndex, endCharIndex + 1).map((t) => t - startTime)
|
|
4621
|
+
// Make relative to section start
|
|
4622
|
+
};
|
|
5325
4623
|
results.push({
|
|
5326
4624
|
id: i + 1,
|
|
5327
4625
|
text: sectionText,
|
|
@@ -5329,7 +4627,9 @@ function calculateSectionTimingFromTimestamps(sections, timestamps, fps) {
|
|
|
5329
4627
|
startTime,
|
|
5330
4628
|
endTime,
|
|
5331
4629
|
durationInSeconds,
|
|
5332
|
-
durationInFrames
|
|
4630
|
+
durationInFrames,
|
|
4631
|
+
timestamps: sectionTimestamps
|
|
4632
|
+
// Add section-specific timestamps
|
|
5333
4633
|
});
|
|
5334
4634
|
}
|
|
5335
4635
|
return results;
|
|
@@ -5354,6 +4654,64 @@ async function readStdin2() {
|
|
|
5354
4654
|
function toFilename(name) {
|
|
5355
4655
|
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
5356
4656
|
}
|
|
4657
|
+
function createTimelineFromScenes(scenes) {
|
|
4658
|
+
const timeline = {
|
|
4659
|
+
shortTitle: "Video",
|
|
4660
|
+
elements: [],
|
|
4661
|
+
audio: [],
|
|
4662
|
+
text: []
|
|
4663
|
+
};
|
|
4664
|
+
let zoomIn = true;
|
|
4665
|
+
scenes.forEach((scene) => {
|
|
4666
|
+
const startMs = scene.startTime * 1e3;
|
|
4667
|
+
const endMs = scene.endTime * 1e3;
|
|
4668
|
+
const durationMs = scene.durationInSeconds * 1e3;
|
|
4669
|
+
if (scene.imagePath) {
|
|
4670
|
+
timeline.elements.push({
|
|
4671
|
+
startMs,
|
|
4672
|
+
endMs,
|
|
4673
|
+
imageUrl: scene.imagePath,
|
|
4674
|
+
enterTransition: "blur",
|
|
4675
|
+
exitTransition: "blur",
|
|
4676
|
+
animations: [{
|
|
4677
|
+
type: "scale",
|
|
4678
|
+
from: zoomIn ? 1.3 : 1,
|
|
4679
|
+
to: zoomIn ? 1 : 1.3,
|
|
4680
|
+
startMs: 0,
|
|
4681
|
+
endMs: durationMs
|
|
4682
|
+
}]
|
|
4683
|
+
});
|
|
4684
|
+
zoomIn = !zoomIn;
|
|
4685
|
+
} else if (scene.videoPath) {
|
|
4686
|
+
timeline.elements.push({
|
|
4687
|
+
startMs,
|
|
4688
|
+
endMs,
|
|
4689
|
+
videoUrl: scene.videoPath,
|
|
4690
|
+
enterTransition: "blur",
|
|
4691
|
+
exitTransition: "blur",
|
|
4692
|
+
animations: []
|
|
4693
|
+
});
|
|
4694
|
+
}
|
|
4695
|
+
if (scene.audioPath) {
|
|
4696
|
+
timeline.audio.push({
|
|
4697
|
+
startMs,
|
|
4698
|
+
endMs,
|
|
4699
|
+
audioUrl: scene.audioPath
|
|
4700
|
+
});
|
|
4701
|
+
}
|
|
4702
|
+
if (scene.timestamps) {
|
|
4703
|
+
timeline.text.push({
|
|
4704
|
+
startMs,
|
|
4705
|
+
endMs,
|
|
4706
|
+
text: scene.text,
|
|
4707
|
+
position: "bottom",
|
|
4708
|
+
animations: [],
|
|
4709
|
+
timestamps: scene.timestamps
|
|
4710
|
+
});
|
|
4711
|
+
}
|
|
4712
|
+
});
|
|
4713
|
+
return timeline;
|
|
4714
|
+
}
|
|
5357
4715
|
async function downloadFile3(url, outputPath) {
|
|
5358
4716
|
if (url.startsWith("data:")) {
|
|
5359
4717
|
const matches = url.match(/^data:[^;]+;base64,(.+)$/);
|
|
@@ -5383,7 +4741,7 @@ function getExtension(url) {
|
|
|
5383
4741
|
}
|
|
5384
4742
|
return "jpg";
|
|
5385
4743
|
}
|
|
5386
|
-
var
|
|
4744
|
+
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) => {
|
|
5387
4745
|
const format = options.format;
|
|
5388
4746
|
const spinner = format === "human" ? ora12("Initializing...").start() : null;
|
|
5389
4747
|
try {
|
|
@@ -5428,21 +4786,27 @@ var createCommand2 = new Command19("create").description("Create video assets (v
|
|
|
5428
4786
|
info(`Processing ${scenesInput.scenes.length} scenes...`);
|
|
5429
4787
|
spinner?.start();
|
|
5430
4788
|
}
|
|
4789
|
+
const ttsRequests = scenesInput.scenes.map((scene, i) => ({
|
|
4790
|
+
text: scene.script,
|
|
4791
|
+
id: `scene-${i}`
|
|
4792
|
+
}));
|
|
4793
|
+
if (spinner) spinner.text = "Generating speech for all scenes...";
|
|
4794
|
+
const batchResult = await generateSpeechBatch({
|
|
4795
|
+
texts: ttsRequests,
|
|
4796
|
+
options: {
|
|
4797
|
+
voice,
|
|
4798
|
+
voiceSettings: scenesInput.voiceSettings
|
|
4799
|
+
}
|
|
4800
|
+
});
|
|
4801
|
+
totalCost += batchResult.totalCost;
|
|
5431
4802
|
let currentTime = 0;
|
|
5432
4803
|
for (let i = 0; i < scenesInput.scenes.length; i++) {
|
|
5433
4804
|
const scene = scenesInput.scenes[i];
|
|
5434
4805
|
const filename = toFilename(scene.name);
|
|
5435
|
-
|
|
5436
|
-
|
|
5437
|
-
text: scene.script,
|
|
5438
|
-
options: {
|
|
5439
|
-
voice,
|
|
5440
|
-
voiceSettings: scenesInput.voiceSettings
|
|
5441
|
-
}
|
|
5442
|
-
});
|
|
4806
|
+
const ttsResult = batchResult.results[i];
|
|
4807
|
+
if (spinner) spinner.text = `[${scene.name}] Saving audio...`;
|
|
5443
4808
|
const audioPath = join2(audioDir, `${filename}.${ttsResult.format}`);
|
|
5444
4809
|
await writeFile5(audioPath, ttsResult.audioData);
|
|
5445
|
-
totalCost += ttsResult.cost;
|
|
5446
4810
|
const durationInSeconds = ttsResult.duration;
|
|
5447
4811
|
const durationInFrames = Math.round(durationInSeconds * DEFAULT_FPS);
|
|
5448
4812
|
const sceneData = {
|
|
@@ -5454,7 +4818,9 @@ var createCommand2 = new Command19("create").description("Create video assets (v
|
|
|
5454
4818
|
endTime: currentTime + durationInSeconds,
|
|
5455
4819
|
durationInSeconds,
|
|
5456
4820
|
durationInFrames,
|
|
5457
|
-
audioPath: `audio/${filename}.${ttsResult.format}
|
|
4821
|
+
audioPath: `audio/${filename}.${ttsResult.format}`,
|
|
4822
|
+
timestamps: ttsResult.timestamps
|
|
4823
|
+
// Character-level timestamps for captions
|
|
5458
4824
|
};
|
|
5459
4825
|
if (scene.imageQuery) {
|
|
5460
4826
|
if (spinner) spinner.text = `[${scene.name}] Searching image...`;
|
|
@@ -5578,66 +4944,93 @@ var createCommand2 = new Command19("create").description("Create video assets (v
|
|
|
5578
4944
|
spinner?.start();
|
|
5579
4945
|
}
|
|
5580
4946
|
}
|
|
5581
|
-
|
|
4947
|
+
if (spinner) spinner.text = "Creating timeline...";
|
|
4948
|
+
const timeline = createTimelineFromScenes(scenes);
|
|
4949
|
+
const videoEndTimeMs = Math.max(
|
|
4950
|
+
timeline.audio.length > 0 ? Math.max(...timeline.audio.map((a) => a.endMs)) : 0,
|
|
4951
|
+
timeline.text.length > 0 ? Math.max(...timeline.text.map((t) => t.endMs)) : 0,
|
|
4952
|
+
timeline.elements.length > 0 ? Math.max(...timeline.elements.map((e) => e.endMs)) : 0
|
|
4953
|
+
);
|
|
4954
|
+
const actualVideoDuration = videoEndTimeMs / 1e3;
|
|
4955
|
+
const musicDuration = Math.min(30, Math.ceil(actualVideoDuration));
|
|
5582
4956
|
console.log(`[Music Generation] Requesting music:`, {
|
|
5583
4957
|
prompt: musicPrompt,
|
|
5584
4958
|
requestedDuration: musicDuration,
|
|
5585
|
-
totalAudioDuration: totalDuration
|
|
5586
|
-
});
|
|
5587
|
-
if (spinner) spinner.text = "Generating music...";
|
|
5588
|
-
let musicResult = await generateMusic({
|
|
5589
|
-
prompt: musicPrompt,
|
|
5590
|
-
duration: musicDuration
|
|
5591
|
-
});
|
|
5592
|
-
if (musicResult.status !== "completed" && musicResult.status !== "failed") {
|
|
5593
|
-
if (spinner) spinner.text = `Processing music (ID: ${musicResult.requestId})...`;
|
|
5594
|
-
musicResult = await pollForCompletion(
|
|
5595
|
-
() => checkMusicStatus(musicResult.requestId),
|
|
5596
|
-
60,
|
|
5597
|
-
2e3
|
|
5598
|
-
);
|
|
5599
|
-
}
|
|
5600
|
-
if (musicResult.status === "failed") {
|
|
5601
|
-
spinner?.stop();
|
|
5602
|
-
error(`Music generation failed: ${musicResult.error || "Unknown error"}`);
|
|
5603
|
-
process.exit(EXIT_CODES.GENERAL_ERROR);
|
|
5604
|
-
}
|
|
5605
|
-
const musicPath = join2(audioDir, "music.mp3");
|
|
5606
|
-
if (musicResult.audioUrl) {
|
|
5607
|
-
await downloadFile3(musicResult.audioUrl, musicPath);
|
|
5608
|
-
}
|
|
5609
|
-
totalCost += musicResult.cost || 0;
|
|
5610
|
-
const actualMusicDuration = musicResult.duration || musicDuration;
|
|
5611
|
-
console.log(`[Music Generation] Received music:`, {
|
|
5612
|
-
requestedDuration: musicDuration,
|
|
5613
|
-
returnedDuration: musicResult.duration,
|
|
5614
|
-
actualUsedDuration: actualMusicDuration,
|
|
5615
4959
|
totalAudioDuration: totalDuration,
|
|
5616
|
-
|
|
5617
|
-
|
|
4960
|
+
actualVideoDuration,
|
|
4961
|
+
timelineDurationMs: videoEndTimeMs
|
|
5618
4962
|
});
|
|
5619
|
-
|
|
5620
|
-
|
|
5621
|
-
|
|
5622
|
-
|
|
5623
|
-
|
|
5624
|
-
|
|
5625
|
-
|
|
5626
|
-
|
|
5627
|
-
|
|
5628
|
-
|
|
5629
|
-
|
|
5630
|
-
|
|
4963
|
+
let musicInfo;
|
|
4964
|
+
if (musicDuration < 3) {
|
|
4965
|
+
if (format === "human") {
|
|
4966
|
+
spinner?.stop();
|
|
4967
|
+
warn(`Video duration (${actualVideoDuration.toFixed(1)}s) is too short for music generation (minimum 3s).`);
|
|
4968
|
+
warn(`Skipping music generation...`);
|
|
4969
|
+
spinner?.start();
|
|
4970
|
+
}
|
|
4971
|
+
} else {
|
|
4972
|
+
try {
|
|
4973
|
+
if (spinner) spinner.text = "Generating music...";
|
|
4974
|
+
let musicResult = await generateMusic({
|
|
4975
|
+
prompt: musicPrompt,
|
|
4976
|
+
duration: musicDuration
|
|
4977
|
+
});
|
|
4978
|
+
if (musicResult.status !== "completed" && musicResult.status !== "failed") {
|
|
4979
|
+
if (spinner) spinner.text = `Processing music (ID: ${musicResult.requestId})...`;
|
|
4980
|
+
musicResult = await pollForCompletion(
|
|
4981
|
+
() => checkMusicStatus(musicResult.requestId),
|
|
4982
|
+
60,
|
|
4983
|
+
2e3
|
|
4984
|
+
);
|
|
4985
|
+
}
|
|
4986
|
+
if (musicResult.status === "failed") {
|
|
4987
|
+
throw new Error(musicResult.error || "Unknown error");
|
|
4988
|
+
}
|
|
4989
|
+
const musicPath = join2(audioDir, "music.mp3");
|
|
4990
|
+
if (musicResult.audioUrl) {
|
|
4991
|
+
await downloadFile3(musicResult.audioUrl, musicPath);
|
|
4992
|
+
}
|
|
4993
|
+
totalCost += musicResult.cost || 0;
|
|
4994
|
+
const actualMusicDuration = musicResult.duration || musicDuration;
|
|
4995
|
+
console.log(`[Music Generation] Received music:`, {
|
|
4996
|
+
requestedDuration: musicDuration,
|
|
4997
|
+
returnedDuration: musicResult.duration,
|
|
4998
|
+
actualUsedDuration: actualMusicDuration,
|
|
4999
|
+
totalAudioDuration: totalDuration,
|
|
5000
|
+
difference: actualMusicDuration - totalDuration,
|
|
5001
|
+
audioUrl: musicResult.audioUrl?.substring(0, 50) + "..."
|
|
5002
|
+
});
|
|
5003
|
+
musicInfo = {
|
|
5004
|
+
path: "audio/music.mp3",
|
|
5005
|
+
duration: actualMusicDuration,
|
|
5006
|
+
prompt: musicPrompt,
|
|
5007
|
+
cost: musicResult.cost || 0
|
|
5008
|
+
};
|
|
5009
|
+
if (format === "human") {
|
|
5010
|
+
spinner?.stop();
|
|
5011
|
+
success(`Music: ${musicPath} (${musicInfo.duration}s)`);
|
|
5012
|
+
if (actualMusicDuration < actualVideoDuration) {
|
|
5013
|
+
warn(`Music duration (${actualMusicDuration.toFixed(1)}s) is shorter than video duration (${actualVideoDuration.toFixed(1)}s).`);
|
|
5014
|
+
warn(`Consider using audio looping or extending music in Remotion.`);
|
|
5015
|
+
}
|
|
5016
|
+
spinner?.start();
|
|
5017
|
+
}
|
|
5018
|
+
} catch (musicError) {
|
|
5019
|
+
spinner?.stop();
|
|
5020
|
+
warn(`Music generation failed: ${musicError.message}`);
|
|
5021
|
+
warn(`Continuing without background music...`);
|
|
5022
|
+
if (spinner && format === "human") spinner?.start();
|
|
5631
5023
|
}
|
|
5632
|
-
spinner?.start();
|
|
5633
5024
|
}
|
|
5634
5025
|
if (spinner) spinner.text = "Writing manifest...";
|
|
5635
|
-
const totalDurationInFrames = Math.round(
|
|
5026
|
+
const totalDurationInFrames = Math.round(actualVideoDuration * DEFAULT_FPS);
|
|
5636
5027
|
const manifest = {
|
|
5637
5028
|
music: musicInfo,
|
|
5638
5029
|
images: allImages,
|
|
5639
5030
|
videos: allVideos,
|
|
5640
5031
|
scenes,
|
|
5032
|
+
timeline,
|
|
5033
|
+
// Include Remotion timeline in manifest
|
|
5641
5034
|
totalDurationInFrames,
|
|
5642
5035
|
fps: DEFAULT_FPS,
|
|
5643
5036
|
totalCost,
|
|
@@ -5666,7 +5059,7 @@ var createCommand2 = new Command19("create").description("Create video assets (v
|
|
|
5666
5059
|
].filter(Boolean).join(", ");
|
|
5667
5060
|
info(` - ${scene.name}: ${scene.durationInSeconds.toFixed(1)}s [${assets}]`);
|
|
5668
5061
|
}
|
|
5669
|
-
info(`Music: ${musicInfo
|
|
5062
|
+
info(`Music: ${musicInfo?.path} (${musicInfo?.duration}s)`);
|
|
5670
5063
|
info(`Manifest: ${manifestPath}`);
|
|
5671
5064
|
console.log();
|
|
5672
5065
|
info(`Total cost: $${totalCost.toFixed(4)}`);
|
|
@@ -5982,10 +5375,10 @@ var thumbnailCommand = new Command19("thumbnail").description("Embed a thumbnail
|
|
|
5982
5375
|
process.exit(EXIT_CODES.GENERAL_ERROR);
|
|
5983
5376
|
}
|
|
5984
5377
|
});
|
|
5985
|
-
var videoCommand = new Command19("video").description("Video asset generation commands").addCommand(initCommand).addCommand(
|
|
5378
|
+
var videoCommand = new Command19("video").description("Video asset generation commands").addCommand(initCommand).addCommand(createCommand3).addCommand(searchCommand2).addCommand(thumbnailCommand);
|
|
5986
5379
|
|
|
5987
5380
|
// src/index.ts
|
|
5988
|
-
var VERSION = "0.1.
|
|
5381
|
+
var VERSION = "0.1.11";
|
|
5989
5382
|
var program = new Command20();
|
|
5990
5383
|
var cmdName = brand.commands[0];
|
|
5991
5384
|
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({
|