@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/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, displayName } = context;
3023
- const envPrefix = name.toUpperCase().replace(/-/g, "_");
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: ${displayName} 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.
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
- # ${displayName} CLI
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 VIDEO_RULE_CONTENTS = [
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
- \`\`\`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
- \`\`\`
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
- ## 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:
3499
+ Can capture: \`pnpm exec remotion still ThumbnailScene out/thumb.png\`
3500
+ `;
3501
+ var MOTION_DESIGN_GUIDELINES = `# Motion Design Principles
4028
3502
 
4029
- \`\`\`tsx
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
- <Audio src={musicSrc} volume={0.3} loop />
4033
- \`\`\`
3505
+ ## Design System Approach
4034
3506
 
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
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
- ## 2. Music Transitions Sound Abrupt
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
- **Problem:** Music cuts harshly when scenes change or video ends.
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
- **Fix in Remotion:**
4047
- \`\`\`tsx
4048
- import { interpolate, Audio } from 'remotion';
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
- // Fade music in/out at scene boundaries
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
- <Audio src={music} volume={musicVolume} />
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
- ## 3. Scene Transitions Too Harsh
4064
-
4065
- **Problem:** Scenes change abruptly without smooth transitions.
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
- - \`style\`: 0.5-0.7 for more expressive delivery
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
- ## 5. Video Duration Mismatch
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
- **Problem:** Brief says 30-45s but video is 20s (because scene duration = voiceover duration).
3563
+ **Core Concept:** "Invisible Ink Rule" - lines draw in (don't fade in), as if hand-drawn in real-time.
4115
3564
 
4116
- **Fixes:**
4117
- 1. **Slow voice:** Use \`speed: 0.85\` in voiceSettings
4118
- 2. **Add padding in Remotion:** Hold last frame, add breathing room
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"
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
- ## 6. Not Using Project UI Components
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
- **Problem:** Generic UI instead of pixel-perfect project components.
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
- **Fix:** In Phase 1 Discovery:
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
- \`\`\`tsx
4137
- // DON'T: Generic button
4138
- <button style={{ background: 'blue' }}>Click</button>
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
- // DO: Match project's actual button
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
- \`\`\`
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
- ## 7. Missing Physics & Lighting
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, displayName } = context;
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
- # ${displayName} Video Creation CLI
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
- **Positioning anchors:**
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
- Before using this skill, ensure you have:
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
- # 1. Render the video (with voiceover and music already included)
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
- ## Asset Generation
3633
+ ## Video Creation Workflow
3634
+
3635
+ ### 1. Generate Assets
4335
3636
 
4336
- Generate voiceover, music, and visual assets for each scene:
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/Hook.mp3\` - scene voiceovers
4372
- - \`public/audio/music.mp3\` - background music (30s max)
4373
- - \`public/video-manifest.json\` - timing and metadata
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
- ## Core Video Rules
3665
+ Scaffold the template and copy assets:
4379
3666
 
4380
- ${VIDEO_RULE_CONTENTS.map((rule) => rule.content).join("\n\n---\n\n")}
3667
+ \`\`\`bash
3668
+ cd .. && ${cmd2} video init my-video
3669
+ cd my-video
3670
+ # Assets are now in public/ directory
3671
+ \`\`\`
4381
3672
 
4382
- ## Useful Commands
3673
+ ### 3. Render Video
4383
3674
 
3675
+ **For landscape videos (16:9):**
4384
3676
  \`\`\`bash
4385
- # Generate video assets
4386
- ${cmd2} video create < scenes.json
4387
- cat scenes.json | ${cmd2} video create --output ./public
3677
+ pnpm exec remotion render FullVideo out/video.mp4
3678
+ \`\`\`
4388
3679
 
4389
- # Initialize Remotion project
4390
- ${cmd2} video init my-video
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
- # Embed thumbnail
4393
- ${cmd2} video thumbnail out/video.mp4 --frame 60
3686
+ ### 4. Generate & Inject Thumbnail
4394
3687
 
4395
- # Search for stock assets
4396
- ${cmd2} images search "mountain landscape" --limit 10
4397
- ${cmd2} videos search "ocean waves" --limit 5
3688
+ ${THUMBNAIL_RULES}
4398
3689
 
4399
- # Generate audio
4400
- ${cmd2} audio generate "Your script here" --voice Kore
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
- ## Best Practices
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
- ## Troubleshooting
3700
+ ${SVG_ANIMATION_GUIDELINES}
4418
3701
 
4419
- If you encounter issues:
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
- For detailed troubleshooting, see "Known Issues" section above.
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, displayName } = context;
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
- # ${displayName} Presentation CLI
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
- displayName: brand.displayName
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 createCommand2 = new Command19("create").description("Create video assets (voiceover per scene, music, images)").option("-s, --script <text>", "Narration script (legacy single-script mode)").option("--script-file <path>", "Path to script file (legacy) or scenes JSON").option("-t, --topic <text>", "Topic for image search").option("-v, --voice <name>", "TTS voice (Kore, Puck, Rachel, alloy)", "Kore").option("-m, --music-prompt <text>", "Music description").option("-n, --num-images <number>", "Number of images to search/download", "5").option("-o, --output <dir>", "Output directory", "./public").option("-f, --format <format>", "Output format: human, json, quiet", "human").action(async (options) => {
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
- if (spinner) spinner.text = `[${scene.name}] Generating speech...`;
5436
- const ttsResult = await generateSpeech({
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
- const musicDuration = Math.min(30, Math.ceil(totalDuration));
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
- difference: actualMusicDuration - totalDuration,
5617
- audioUrl: musicResult.audioUrl?.substring(0, 50) + "..."
4960
+ actualVideoDuration,
4961
+ timelineDurationMs: videoEndTimeMs
5618
4962
  });
5619
- const musicInfo = {
5620
- path: "audio/music.mp3",
5621
- duration: actualMusicDuration,
5622
- prompt: musicPrompt,
5623
- cost: musicResult.cost || 0
5624
- };
5625
- if (format === "human") {
5626
- spinner?.stop();
5627
- success(`Music: ${musicPath} (${musicInfo.duration}s)`);
5628
- if (actualMusicDuration < totalDuration) {
5629
- warn(`Music duration (${actualMusicDuration.toFixed(1)}s) is shorter than video duration (${totalDuration.toFixed(1)}s).`);
5630
- warn(`Consider using audio looping or extending music in Remotion.`);
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(totalDuration * DEFAULT_FPS);
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.path} (${musicInfo.duration}s)`);
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(createCommand2).addCommand(searchCommand2).addCommand(thumbnailCommand);
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.10";
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({