@conceptcraft/mindframes 0.1.10 → 0.1.12
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 +395 -1001
- 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
|
|
|
@@ -3377,10 +3422,10 @@ cat scenes.json | ${cmd2} video create -o product-demo/public
|
|
|
3377
3422
|
|
|
3378
3423
|
# 3. Implement in Remotion (see ${name}-video skill)
|
|
3379
3424
|
|
|
3380
|
-
# 4. Render and add thumbnail
|
|
3425
|
+
# 4. Render and add thumbnail (version files: v1, v2, v3...)
|
|
3381
3426
|
cd product-demo
|
|
3382
|
-
pnpm exec remotion render FullVideo
|
|
3383
|
-
${cmd2} video thumbnail out/FullVideo.mp4 --frame 60
|
|
3427
|
+
pnpm exec remotion render FullVideo out/FullVideo-v1.mp4
|
|
3428
|
+
${cmd2} video thumbnail out/FullVideo-v1.mp4 --frame 60
|
|
3384
3429
|
\`\`\`
|
|
3385
3430
|
|
|
3386
3431
|
### Presentation from Research
|
|
@@ -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
|
-
\`\`\`
|
|
3487
|
+
var THUMBNAIL_RULES = `Consider creating separate thumbnail component with Remotion (can be captured with remotion still, not used in actual video).
|
|
3482
3488
|
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
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.**
|
|
3540
|
-
|
|
3541
|
-
\`\`\`tsx
|
|
3542
|
-
const STAGGER_FRAMES = 3; // 3-5 frames between items
|
|
3543
|
-
|
|
3544
|
-
{items.map((item, i) => {
|
|
3545
|
-
const delay = i * STAGGER_FRAMES;
|
|
3546
|
-
const progress = spring({
|
|
3547
|
-
frame: frame - delay,
|
|
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
|
-
\`\`\`
|
|
3627
|
-
|
|
3628
|
-
---
|
|
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
|
|
3629
3496
|
|
|
3630
|
-
|
|
3497
|
+
**Specs:** 1280x720, <2MB, 16:9 ratio
|
|
3631
3498
|
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
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
|
-
\`\`\`
|
|
3908
|
-
|
|
3909
|
-
---
|
|
3910
|
-
|
|
3911
|
-
## Zod Schema
|
|
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:
|
|
4028
|
-
|
|
4029
|
-
\`\`\`tsx
|
|
4030
|
-
import { Audio } from 'remotion';
|
|
4031
|
-
|
|
4032
|
-
<Audio src={musicSrc} volume={0.3} loop />
|
|
4033
|
-
\`\`\`
|
|
4034
|
-
|
|
4035
|
-
**How it works:**
|
|
4036
|
-
- Music automatically loops to fill video duration
|
|
4037
|
-
- Set volume to 0.3 (30% - less loud than voice)
|
|
4038
|
-
- Add fade out at the end for smooth ending
|
|
4039
|
-
|
|
4040
|
-
---
|
|
3499
|
+
Can capture: \`pnpm exec remotion still ThumbnailScene out/thumb.png\`
|
|
3500
|
+
`;
|
|
3501
|
+
var MOTION_DESIGN_GUIDELINES = `# Motion Design Principles
|
|
4041
3502
|
|
|
4042
|
-
|
|
3503
|
+
**Core Philosophy:** "Atomic, Kinetic Construction" - nothing is static. Elements arrive and leave via physics-based transitions.
|
|
4043
3504
|
|
|
4044
|
-
|
|
3505
|
+
## Design System Approach
|
|
4045
3506
|
|
|
4046
|
-
**
|
|
4047
|
-
|
|
4048
|
-
|
|
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
|
|
4049
3511
|
|
|
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
|
-
);
|
|
3512
|
+
## Animation Physics (Spring-Based)
|
|
4057
3513
|
|
|
4058
|
-
|
|
4059
|
-
|
|
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)
|
|
4060
3519
|
|
|
4061
|
-
|
|
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
|
|
4062
3525
|
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
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
|
-
\`\`\`
|
|
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
|
|
4086
3533
|
|
|
4087
|
-
|
|
3534
|
+
## Visual Composition
|
|
4088
3535
|
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
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
|
-
\`\`\`
|
|
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
|
|
4105
3540
|
|
|
4106
|
-
|
|
4107
|
-
-
|
|
4108
|
-
-
|
|
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
|
|
4109
3545
|
|
|
4110
|
-
|
|
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
|
|
4111
3550
|
|
|
4112
|
-
##
|
|
3551
|
+
## Suggested Component Architecture
|
|
4113
3552
|
|
|
4114
|
-
|
|
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
|
|
4115
3558
|
|
|
4116
|
-
**
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
\`\`\`tsx
|
|
4120
|
-
// Add 30 frames (0.5s) padding after voiceover ends
|
|
4121
|
-
const paddedDuration = voiceoverFrames + 30;
|
|
4122
|
-
\`\`\`
|
|
4123
|
-
3. **Brief should note:** "Duration based on voiceover length"
|
|
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)
|
|
4124
3562
|
|
|
4125
|
-
|
|
3563
|
+
**Core Concept:** "Invisible Ink Rule" - lines draw in (don't fade in), as if hand-drawn in real-time.
|
|
4126
3564
|
|
|
4127
|
-
|
|
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
|
|
4128
3570
|
|
|
4129
|
-
**
|
|
3571
|
+
**Draw & vanish sequence:**
|
|
3572
|
+
- Draw in: 20 frames (offset 1\u21920)
|
|
3573
|
+
- Hold: 10 frames
|
|
3574
|
+
- Draw out: fade opacity or continue offset
|
|
4130
3575
|
|
|
4131
|
-
**
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
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
|
|
4135
3582
|
|
|
4136
|
-
|
|
4137
|
-
// DON'T: Generic button
|
|
4138
|
-
<button style={{ background: 'blue' }}>Click</button>
|
|
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.
|
|
4139
3584
|
|
|
4140
|
-
|
|
4141
|
-
<button style={{
|
|
4142
|
-
background: 'linear-gradient(135deg, #6366f1, #8b5cf6)',
|
|
4143
|
-
borderRadius: 8,
|
|
4144
|
-
padding: '12px 24px',
|
|
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
|
-
\`\`\`
|
|
3585
|
+
**CLI provides flexible asset search** - images and videos can be used creatively throughout compositions.
|
|
4149
3586
|
|
|
4150
|
-
|
|
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
|
|
4151
3593
|
|
|
4152
|
-
|
|
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
|
|
4153
3599
|
|
|
4154
|
-
**
|
|
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
|
|
4155
3606
|
|
|
4156
|
-
**
|
|
4157
|
-
|
|
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,83 @@ 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
|
+
\`\`\`
|
|
3672
|
+
|
|
3673
|
+
### 3. Render Video
|
|
4381
3674
|
|
|
4382
|
-
|
|
3675
|
+
**IMPORTANT:** Always version output files to avoid overwriting. Check existing files and increment version:
|
|
3676
|
+
- First render: \`out/video-v1.mp4\`
|
|
3677
|
+
- Re-render: \`out/video-v2.mp4\`, \`out/video-v3.mp4\`, etc.
|
|
4383
3678
|
|
|
3679
|
+
**For landscape videos (16:9):**
|
|
4384
3680
|
\`\`\`bash
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
cat scenes.json | ${cmd2} video create --output ./public
|
|
3681
|
+
pnpm exec remotion render FullVideo out/video-v1.mp4
|
|
3682
|
+
\`\`\`
|
|
4388
3683
|
|
|
4389
|
-
|
|
4390
|
-
|
|
3684
|
+
**For vertical videos (9:16) with captions:**
|
|
3685
|
+
\`\`\`bash
|
|
3686
|
+
pnpm exec remotion render CaptionedVideo out/tiktok-v1.mp4 \\
|
|
3687
|
+
--props='{"timeline":'$(cat public/video-manifest.json | jq -c .timeline)',"showCaptions":true}'
|
|
3688
|
+
\`\`\`
|
|
4391
3689
|
|
|
4392
|
-
|
|
4393
|
-
${cmd2} video thumbnail out/video.mp4 --frame 60
|
|
3690
|
+
### 4. Generate & Inject Thumbnail
|
|
4394
3691
|
|
|
4395
|
-
|
|
4396
|
-
${cmd2} images search "mountain landscape" --limit 10
|
|
4397
|
-
${cmd2} videos search "ocean waves" --limit 5
|
|
3692
|
+
${THUMBNAIL_RULES}
|
|
4398
3693
|
|
|
4399
|
-
|
|
4400
|
-
${cmd2}
|
|
4401
|
-
${cmd2} music generate "upbeat corporate" --duration 30
|
|
3694
|
+
\`\`\`bash
|
|
3695
|
+
${cmd2} video thumbnail out/video.mp4 --image out/thumb.png
|
|
4402
3696
|
\`\`\`
|
|
4403
3697
|
|
|
4404
3698
|
---
|
|
4405
3699
|
|
|
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
|
|
3700
|
+
${MOTION_DESIGN_GUIDELINES}
|
|
4414
3701
|
|
|
4415
3702
|
---
|
|
4416
3703
|
|
|
4417
|
-
|
|
3704
|
+
${SVG_ANIMATION_GUIDELINES}
|
|
3705
|
+
|
|
3706
|
+
---
|
|
4418
3707
|
|
|
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
|
|
3708
|
+
${ASSET_USAGE_GUIDELINES}
|
|
4424
3709
|
|
|
4425
|
-
|
|
3710
|
+
---
|
|
4426
3711
|
`;
|
|
4427
3712
|
}
|
|
4428
3713
|
|
|
4429
3714
|
// src/commands/skill/generate-presentation-skill.ts
|
|
4430
3715
|
function generatePresentationSkillContent(context) {
|
|
4431
|
-
const { name, cmd: cmd2
|
|
3716
|
+
const { name, cmd: cmd2 } = context;
|
|
4432
3717
|
return `---
|
|
4433
3718
|
name: ${name}-presentation
|
|
4434
3719
|
description: Use when user asks to create presentations (slides, decks, pitch decks). Generates AI-powered presentations with structured content and professional design.
|
|
4435
3720
|
---
|
|
4436
3721
|
|
|
4437
|
-
# ${
|
|
3722
|
+
# ${name} Presentation Creation CLI
|
|
4438
3723
|
|
|
4439
3724
|
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
3725
|
|
|
@@ -4701,9 +3986,10 @@ function getSupportedEditorNames() {
|
|
|
4701
3986
|
// src/commands/skill/index.ts
|
|
4702
3987
|
var SKILL_TYPES = ["main", "video", "presentation"];
|
|
4703
3988
|
var skillContext = {
|
|
4704
|
-
name: brand.name,
|
|
4705
3989
|
cmd: brand.commands[0],
|
|
4706
|
-
|
|
3990
|
+
pkg: brand.packageName,
|
|
3991
|
+
url: brand.apiUrl,
|
|
3992
|
+
name: brand.displayName
|
|
4707
3993
|
};
|
|
4708
3994
|
var skillCommand = new Command14("skill").description(`Manage ${brand.displayName} skills for AI coding assistants`).addHelpText(
|
|
4709
3995
|
"after",
|
|
@@ -4835,7 +4121,8 @@ skillCommand.command("uninstall").description(`Remove ${brand.displayName} skill
|
|
|
4835
4121
|
import { Command as Command15 } from "commander";
|
|
4836
4122
|
import ora8 from "ora";
|
|
4837
4123
|
import { writeFile as writeFile2 } from "fs/promises";
|
|
4838
|
-
var
|
|
4124
|
+
var DEFAULT_ELEVENLABS_VOICE_ID = "21m00Tcm4TlvDq8ikWAM";
|
|
4125
|
+
var generateCommand = new Command15("generate").description("Generate speech from text (uses ElevenLabs)").requiredOption("-t, --text <text>", "Text to convert to speech").requiredOption("-o, --output <path>", "Output file path").option("--voice-id <voiceId>", `ElevenLabs voice ID (default: ${DEFAULT_ELEVENLABS_VOICE_ID})`).option("-m, --model <model>", "Model (provider-specific)").option("-s, --speed <speed>", "Speech speed 0.25-4.0 (default: 1.0)").option("-f, --format <format>", "Output format: human, json, quiet", "human").action(async (options) => {
|
|
4839
4126
|
const format = options.format;
|
|
4840
4127
|
const spinner = format === "human" ? ora8("Generating speech...").start() : null;
|
|
4841
4128
|
let speed;
|
|
@@ -4848,11 +4135,13 @@ var generateCommand = new Command15("generate").description("Generate speech fro
|
|
|
4848
4135
|
}
|
|
4849
4136
|
}
|
|
4850
4137
|
try {
|
|
4138
|
+
const voiceId = options.voiceId || DEFAULT_ELEVENLABS_VOICE_ID;
|
|
4851
4139
|
const result = await generateSpeech({
|
|
4852
4140
|
text: options.text,
|
|
4853
4141
|
options: {
|
|
4854
|
-
provider:
|
|
4855
|
-
|
|
4142
|
+
provider: "elevenlabs",
|
|
4143
|
+
// Currently only ElevenLabs is supported
|
|
4144
|
+
voiceId,
|
|
4856
4145
|
model: options.model,
|
|
4857
4146
|
speed
|
|
4858
4147
|
}
|
|
@@ -4885,31 +4174,28 @@ var generateCommand = new Command15("generate").description("Generate speech fro
|
|
|
4885
4174
|
process.exit(EXIT_CODES.GENERAL_ERROR);
|
|
4886
4175
|
}
|
|
4887
4176
|
});
|
|
4888
|
-
var voicesCommand = new Command15("voices").description("List available voices
|
|
4177
|
+
var voicesCommand = new Command15("voices").description("List available voices (currently only ElevenLabs)").option("-f, --format <format>", "Output format: human, json", "human").action(async (options) => {
|
|
4889
4178
|
const spinner = options.format === "human" ? ora8("Fetching voices...").start() : null;
|
|
4890
4179
|
try {
|
|
4891
4180
|
const result = await getVoices();
|
|
4892
4181
|
spinner?.stop();
|
|
4182
|
+
const provider = "elevenlabs";
|
|
4893
4183
|
if (options.format === "json") {
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
printJson(providerVoices || []);
|
|
4897
|
-
} else {
|
|
4898
|
-
printJson(result.voices);
|
|
4899
|
-
}
|
|
4184
|
+
const providerVoices = result.voices[provider];
|
|
4185
|
+
printJson(providerVoices || []);
|
|
4900
4186
|
return;
|
|
4901
4187
|
}
|
|
4902
|
-
const
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
}
|
|
4188
|
+
const voices = result.voices[provider];
|
|
4189
|
+
if (!voices || voices.length === 0) {
|
|
4190
|
+
info("No ElevenLabs voices available");
|
|
4191
|
+
return;
|
|
4192
|
+
}
|
|
4193
|
+
console.log();
|
|
4194
|
+
console.log(`ELEVENLABS Voices:`);
|
|
4195
|
+
console.log("-".repeat(50));
|
|
4196
|
+
for (const voice of voices) {
|
|
4197
|
+
console.log(` ${voice.name} (${voice.id})`);
|
|
4198
|
+
console.log(` ${voice.description}`);
|
|
4913
4199
|
}
|
|
4914
4200
|
} catch (err) {
|
|
4915
4201
|
spinner?.stop();
|
|
@@ -5288,6 +4574,13 @@ function calculateSectionTiming(sections, totalDuration, fps = DEFAULT_FPS, time
|
|
|
5288
4574
|
const proportion = wordCount / totalWords;
|
|
5289
4575
|
const durationInSeconds = totalDuration * proportion;
|
|
5290
4576
|
const durationInFrames = Math.round(durationInSeconds * fps);
|
|
4577
|
+
const chars = text.split("");
|
|
4578
|
+
const charDuration = durationInSeconds / chars.length;
|
|
4579
|
+
const approximateTimestamps = {
|
|
4580
|
+
characters: chars,
|
|
4581
|
+
characterStartTimesSeconds: chars.map((_, i) => i * charDuration),
|
|
4582
|
+
characterEndTimesSeconds: chars.map((_, i) => (i + 1) * charDuration)
|
|
4583
|
+
};
|
|
5291
4584
|
const section = {
|
|
5292
4585
|
id: index + 1,
|
|
5293
4586
|
text,
|
|
@@ -5295,7 +4588,9 @@ function calculateSectionTiming(sections, totalDuration, fps = DEFAULT_FPS, time
|
|
|
5295
4588
|
startTime: currentTime,
|
|
5296
4589
|
endTime: currentTime + durationInSeconds,
|
|
5297
4590
|
durationInSeconds,
|
|
5298
|
-
durationInFrames
|
|
4591
|
+
durationInFrames,
|
|
4592
|
+
timestamps: approximateTimestamps
|
|
4593
|
+
// Always include timestamps (approximate if needed)
|
|
5299
4594
|
};
|
|
5300
4595
|
currentTime += durationInSeconds;
|
|
5301
4596
|
return section;
|
|
@@ -5322,6 +4617,13 @@ function calculateSectionTimingFromTimestamps(sections, timestamps, fps) {
|
|
|
5322
4617
|
const endTime = characterEndTimesSeconds[Math.min(endCharIndex, characterEndTimesSeconds.length - 1)] || startTime + 1;
|
|
5323
4618
|
const durationInSeconds = endTime - startTime;
|
|
5324
4619
|
const durationInFrames = Math.round(durationInSeconds * fps);
|
|
4620
|
+
const sectionTimestamps = {
|
|
4621
|
+
characters: characters.slice(startCharIndex, endCharIndex + 1),
|
|
4622
|
+
characterStartTimesSeconds: characterStartTimesSeconds.slice(startCharIndex, endCharIndex + 1).map((t) => t - startTime),
|
|
4623
|
+
// Make relative to section start
|
|
4624
|
+
characterEndTimesSeconds: characterEndTimesSeconds.slice(startCharIndex, endCharIndex + 1).map((t) => t - startTime)
|
|
4625
|
+
// Make relative to section start
|
|
4626
|
+
};
|
|
5325
4627
|
results.push({
|
|
5326
4628
|
id: i + 1,
|
|
5327
4629
|
text: sectionText,
|
|
@@ -5329,7 +4631,9 @@ function calculateSectionTimingFromTimestamps(sections, timestamps, fps) {
|
|
|
5329
4631
|
startTime,
|
|
5330
4632
|
endTime,
|
|
5331
4633
|
durationInSeconds,
|
|
5332
|
-
durationInFrames
|
|
4634
|
+
durationInFrames,
|
|
4635
|
+
timestamps: sectionTimestamps
|
|
4636
|
+
// Add section-specific timestamps
|
|
5333
4637
|
});
|
|
5334
4638
|
}
|
|
5335
4639
|
return results;
|
|
@@ -5354,6 +4658,64 @@ async function readStdin2() {
|
|
|
5354
4658
|
function toFilename(name) {
|
|
5355
4659
|
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
5356
4660
|
}
|
|
4661
|
+
function createTimelineFromScenes(scenes) {
|
|
4662
|
+
const timeline = {
|
|
4663
|
+
shortTitle: "Video",
|
|
4664
|
+
elements: [],
|
|
4665
|
+
audio: [],
|
|
4666
|
+
text: []
|
|
4667
|
+
};
|
|
4668
|
+
let zoomIn = true;
|
|
4669
|
+
scenes.forEach((scene) => {
|
|
4670
|
+
const startMs = scene.startTime * 1e3;
|
|
4671
|
+
const endMs = scene.endTime * 1e3;
|
|
4672
|
+
const durationMs = scene.durationInSeconds * 1e3;
|
|
4673
|
+
if (scene.imagePath) {
|
|
4674
|
+
timeline.elements.push({
|
|
4675
|
+
startMs,
|
|
4676
|
+
endMs,
|
|
4677
|
+
imageUrl: scene.imagePath,
|
|
4678
|
+
enterTransition: "blur",
|
|
4679
|
+
exitTransition: "blur",
|
|
4680
|
+
animations: [{
|
|
4681
|
+
type: "scale",
|
|
4682
|
+
from: zoomIn ? 1.3 : 1,
|
|
4683
|
+
to: zoomIn ? 1 : 1.3,
|
|
4684
|
+
startMs: 0,
|
|
4685
|
+
endMs: durationMs
|
|
4686
|
+
}]
|
|
4687
|
+
});
|
|
4688
|
+
zoomIn = !zoomIn;
|
|
4689
|
+
} else if (scene.videoPath) {
|
|
4690
|
+
timeline.elements.push({
|
|
4691
|
+
startMs,
|
|
4692
|
+
endMs,
|
|
4693
|
+
videoUrl: scene.videoPath,
|
|
4694
|
+
enterTransition: "blur",
|
|
4695
|
+
exitTransition: "blur",
|
|
4696
|
+
animations: []
|
|
4697
|
+
});
|
|
4698
|
+
}
|
|
4699
|
+
if (scene.audioPath) {
|
|
4700
|
+
timeline.audio.push({
|
|
4701
|
+
startMs,
|
|
4702
|
+
endMs,
|
|
4703
|
+
audioUrl: scene.audioPath
|
|
4704
|
+
});
|
|
4705
|
+
}
|
|
4706
|
+
if (scene.timestamps) {
|
|
4707
|
+
timeline.text.push({
|
|
4708
|
+
startMs,
|
|
4709
|
+
endMs,
|
|
4710
|
+
text: scene.text,
|
|
4711
|
+
position: "bottom",
|
|
4712
|
+
animations: [],
|
|
4713
|
+
timestamps: scene.timestamps
|
|
4714
|
+
});
|
|
4715
|
+
}
|
|
4716
|
+
});
|
|
4717
|
+
return timeline;
|
|
4718
|
+
}
|
|
5357
4719
|
async function downloadFile3(url, outputPath) {
|
|
5358
4720
|
if (url.startsWith("data:")) {
|
|
5359
4721
|
const matches = url.match(/^data:[^;]+;base64,(.+)$/);
|
|
@@ -5383,7 +4745,7 @@ function getExtension(url) {
|
|
|
5383
4745
|
}
|
|
5384
4746
|
return "jpg";
|
|
5385
4747
|
}
|
|
5386
|
-
var
|
|
4748
|
+
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
4749
|
const format = options.format;
|
|
5388
4750
|
const spinner = format === "human" ? ora12("Initializing...").start() : null;
|
|
5389
4751
|
try {
|
|
@@ -5428,21 +4790,27 @@ var createCommand2 = new Command19("create").description("Create video assets (v
|
|
|
5428
4790
|
info(`Processing ${scenesInput.scenes.length} scenes...`);
|
|
5429
4791
|
spinner?.start();
|
|
5430
4792
|
}
|
|
4793
|
+
const ttsRequests = scenesInput.scenes.map((scene, i) => ({
|
|
4794
|
+
text: scene.script,
|
|
4795
|
+
id: `scene-${i}`
|
|
4796
|
+
}));
|
|
4797
|
+
if (spinner) spinner.text = "Generating speech for all scenes...";
|
|
4798
|
+
const batchResult = await generateSpeechBatch({
|
|
4799
|
+
texts: ttsRequests,
|
|
4800
|
+
options: {
|
|
4801
|
+
voice,
|
|
4802
|
+
voiceSettings: scenesInput.voiceSettings
|
|
4803
|
+
}
|
|
4804
|
+
});
|
|
4805
|
+
totalCost += batchResult.totalCost;
|
|
5431
4806
|
let currentTime = 0;
|
|
5432
4807
|
for (let i = 0; i < scenesInput.scenes.length; i++) {
|
|
5433
4808
|
const scene = scenesInput.scenes[i];
|
|
5434
4809
|
const filename = toFilename(scene.name);
|
|
5435
|
-
|
|
5436
|
-
|
|
5437
|
-
text: scene.script,
|
|
5438
|
-
options: {
|
|
5439
|
-
voice,
|
|
5440
|
-
voiceSettings: scenesInput.voiceSettings
|
|
5441
|
-
}
|
|
5442
|
-
});
|
|
4810
|
+
const ttsResult = batchResult.results[i];
|
|
4811
|
+
if (spinner) spinner.text = `[${scene.name}] Saving audio...`;
|
|
5443
4812
|
const audioPath = join2(audioDir, `${filename}.${ttsResult.format}`);
|
|
5444
4813
|
await writeFile5(audioPath, ttsResult.audioData);
|
|
5445
|
-
totalCost += ttsResult.cost;
|
|
5446
4814
|
const durationInSeconds = ttsResult.duration;
|
|
5447
4815
|
const durationInFrames = Math.round(durationInSeconds * DEFAULT_FPS);
|
|
5448
4816
|
const sceneData = {
|
|
@@ -5454,7 +4822,9 @@ var createCommand2 = new Command19("create").description("Create video assets (v
|
|
|
5454
4822
|
endTime: currentTime + durationInSeconds,
|
|
5455
4823
|
durationInSeconds,
|
|
5456
4824
|
durationInFrames,
|
|
5457
|
-
audioPath: `audio/${filename}.${ttsResult.format}
|
|
4825
|
+
audioPath: `audio/${filename}.${ttsResult.format}`,
|
|
4826
|
+
timestamps: ttsResult.timestamps
|
|
4827
|
+
// Character-level timestamps for captions
|
|
5458
4828
|
};
|
|
5459
4829
|
if (scene.imageQuery) {
|
|
5460
4830
|
if (spinner) spinner.text = `[${scene.name}] Searching image...`;
|
|
@@ -5578,66 +4948,93 @@ var createCommand2 = new Command19("create").description("Create video assets (v
|
|
|
5578
4948
|
spinner?.start();
|
|
5579
4949
|
}
|
|
5580
4950
|
}
|
|
5581
|
-
|
|
4951
|
+
if (spinner) spinner.text = "Creating timeline...";
|
|
4952
|
+
const timeline = createTimelineFromScenes(scenes);
|
|
4953
|
+
const videoEndTimeMs = Math.max(
|
|
4954
|
+
timeline.audio.length > 0 ? Math.max(...timeline.audio.map((a) => a.endMs)) : 0,
|
|
4955
|
+
timeline.text.length > 0 ? Math.max(...timeline.text.map((t) => t.endMs)) : 0,
|
|
4956
|
+
timeline.elements.length > 0 ? Math.max(...timeline.elements.map((e) => e.endMs)) : 0
|
|
4957
|
+
);
|
|
4958
|
+
const actualVideoDuration = videoEndTimeMs / 1e3;
|
|
4959
|
+
const musicDuration = Math.min(30, Math.ceil(actualVideoDuration));
|
|
5582
4960
|
console.log(`[Music Generation] Requesting music:`, {
|
|
5583
4961
|
prompt: musicPrompt,
|
|
5584
4962
|
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
4963
|
totalAudioDuration: totalDuration,
|
|
5616
|
-
|
|
5617
|
-
|
|
4964
|
+
actualVideoDuration,
|
|
4965
|
+
timelineDurationMs: videoEndTimeMs
|
|
5618
4966
|
});
|
|
5619
|
-
|
|
5620
|
-
|
|
5621
|
-
|
|
5622
|
-
|
|
5623
|
-
|
|
5624
|
-
|
|
5625
|
-
|
|
5626
|
-
|
|
5627
|
-
|
|
5628
|
-
|
|
5629
|
-
|
|
5630
|
-
|
|
4967
|
+
let musicInfo;
|
|
4968
|
+
if (musicDuration < 3) {
|
|
4969
|
+
if (format === "human") {
|
|
4970
|
+
spinner?.stop();
|
|
4971
|
+
warn(`Video duration (${actualVideoDuration.toFixed(1)}s) is too short for music generation (minimum 3s).`);
|
|
4972
|
+
warn(`Skipping music generation...`);
|
|
4973
|
+
spinner?.start();
|
|
4974
|
+
}
|
|
4975
|
+
} else {
|
|
4976
|
+
try {
|
|
4977
|
+
if (spinner) spinner.text = "Generating music...";
|
|
4978
|
+
let musicResult = await generateMusic({
|
|
4979
|
+
prompt: musicPrompt,
|
|
4980
|
+
duration: musicDuration
|
|
4981
|
+
});
|
|
4982
|
+
if (musicResult.status !== "completed" && musicResult.status !== "failed") {
|
|
4983
|
+
if (spinner) spinner.text = `Processing music (ID: ${musicResult.requestId})...`;
|
|
4984
|
+
musicResult = await pollForCompletion(
|
|
4985
|
+
() => checkMusicStatus(musicResult.requestId),
|
|
4986
|
+
60,
|
|
4987
|
+
2e3
|
|
4988
|
+
);
|
|
4989
|
+
}
|
|
4990
|
+
if (musicResult.status === "failed") {
|
|
4991
|
+
throw new Error(musicResult.error || "Unknown error");
|
|
4992
|
+
}
|
|
4993
|
+
const musicPath = join2(audioDir, "music.mp3");
|
|
4994
|
+
if (musicResult.audioUrl) {
|
|
4995
|
+
await downloadFile3(musicResult.audioUrl, musicPath);
|
|
4996
|
+
}
|
|
4997
|
+
totalCost += musicResult.cost || 0;
|
|
4998
|
+
const actualMusicDuration = musicResult.duration || musicDuration;
|
|
4999
|
+
console.log(`[Music Generation] Received music:`, {
|
|
5000
|
+
requestedDuration: musicDuration,
|
|
5001
|
+
returnedDuration: musicResult.duration,
|
|
5002
|
+
actualUsedDuration: actualMusicDuration,
|
|
5003
|
+
totalAudioDuration: totalDuration,
|
|
5004
|
+
difference: actualMusicDuration - totalDuration,
|
|
5005
|
+
audioUrl: musicResult.audioUrl?.substring(0, 50) + "..."
|
|
5006
|
+
});
|
|
5007
|
+
musicInfo = {
|
|
5008
|
+
path: "audio/music.mp3",
|
|
5009
|
+
duration: actualMusicDuration,
|
|
5010
|
+
prompt: musicPrompt,
|
|
5011
|
+
cost: musicResult.cost || 0
|
|
5012
|
+
};
|
|
5013
|
+
if (format === "human") {
|
|
5014
|
+
spinner?.stop();
|
|
5015
|
+
success(`Music: ${musicPath} (${musicInfo.duration}s)`);
|
|
5016
|
+
if (actualMusicDuration < actualVideoDuration) {
|
|
5017
|
+
warn(`Music duration (${actualMusicDuration.toFixed(1)}s) is shorter than video duration (${actualVideoDuration.toFixed(1)}s).`);
|
|
5018
|
+
warn(`Consider using audio looping or extending music in Remotion.`);
|
|
5019
|
+
}
|
|
5020
|
+
spinner?.start();
|
|
5021
|
+
}
|
|
5022
|
+
} catch (musicError) {
|
|
5023
|
+
spinner?.stop();
|
|
5024
|
+
warn(`Music generation failed: ${musicError.message}`);
|
|
5025
|
+
warn(`Continuing without background music...`);
|
|
5026
|
+
if (spinner && format === "human") spinner?.start();
|
|
5631
5027
|
}
|
|
5632
|
-
spinner?.start();
|
|
5633
5028
|
}
|
|
5634
5029
|
if (spinner) spinner.text = "Writing manifest...";
|
|
5635
|
-
const totalDurationInFrames = Math.round(
|
|
5030
|
+
const totalDurationInFrames = Math.round(actualVideoDuration * DEFAULT_FPS);
|
|
5636
5031
|
const manifest = {
|
|
5637
5032
|
music: musicInfo,
|
|
5638
5033
|
images: allImages,
|
|
5639
5034
|
videos: allVideos,
|
|
5640
5035
|
scenes,
|
|
5036
|
+
timeline,
|
|
5037
|
+
// Include Remotion timeline in manifest
|
|
5641
5038
|
totalDurationInFrames,
|
|
5642
5039
|
fps: DEFAULT_FPS,
|
|
5643
5040
|
totalCost,
|
|
@@ -5666,7 +5063,7 @@ var createCommand2 = new Command19("create").description("Create video assets (v
|
|
|
5666
5063
|
].filter(Boolean).join(", ");
|
|
5667
5064
|
info(` - ${scene.name}: ${scene.durationInSeconds.toFixed(1)}s [${assets}]`);
|
|
5668
5065
|
}
|
|
5669
|
-
info(`Music: ${musicInfo
|
|
5066
|
+
info(`Music: ${musicInfo?.path} (${musicInfo?.duration}s)`);
|
|
5670
5067
|
info(`Manifest: ${manifestPath}`);
|
|
5671
5068
|
console.log();
|
|
5672
5069
|
info(`Total cost: $${totalCost.toFixed(4)}`);
|
|
@@ -5791,9 +5188,6 @@ var initCommand = new Command19("init").description("Create a new Remotion video
|
|
|
5791
5188
|
).replace(
|
|
5792
5189
|
/export const VIDEO_HEIGHT = 1080;/,
|
|
5793
5190
|
"export const VIDEO_HEIGHT = 1920; // TikTok 9:16"
|
|
5794
|
-
).replace(
|
|
5795
|
-
/export const VIDEO_FPS = 60;/,
|
|
5796
|
-
"export const VIDEO_FPS = 30; // TikTok standard"
|
|
5797
5191
|
);
|
|
5798
5192
|
await writeFile5(constantsPath, constantsContent, "utf-8");
|
|
5799
5193
|
if (format === "human") {
|
|
@@ -5824,7 +5218,7 @@ var initCommand = new Command19("init").description("Create a new Remotion video
|
|
|
5824
5218
|
if (options.type === "tiktok") {
|
|
5825
5219
|
info("Format: TikTok/Reels/Shorts (1080x1920 @ 30fps)");
|
|
5826
5220
|
} else {
|
|
5827
|
-
info("Format: Landscape (1920x1080 @
|
|
5221
|
+
info("Format: Landscape (1920x1080 @ 30fps)");
|
|
5828
5222
|
}
|
|
5829
5223
|
console.log();
|
|
5830
5224
|
info("Next steps:");
|
|
@@ -5982,10 +5376,10 @@ var thumbnailCommand = new Command19("thumbnail").description("Embed a thumbnail
|
|
|
5982
5376
|
process.exit(EXIT_CODES.GENERAL_ERROR);
|
|
5983
5377
|
}
|
|
5984
5378
|
});
|
|
5985
|
-
var videoCommand = new Command19("video").description("Video asset generation commands").addCommand(initCommand).addCommand(
|
|
5379
|
+
var videoCommand = new Command19("video").description("Video asset generation commands").addCommand(initCommand).addCommand(createCommand3).addCommand(searchCommand2).addCommand(thumbnailCommand);
|
|
5986
5380
|
|
|
5987
5381
|
// src/index.ts
|
|
5988
|
-
var VERSION = "0.1.
|
|
5382
|
+
var VERSION = "0.1.12";
|
|
5989
5383
|
var program = new Command20();
|
|
5990
5384
|
var cmdName = brand.commands[0];
|
|
5991
5385
|
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({
|