@vibeframe/cli 0.57.2 → 0.58.0

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.
Files changed (2) hide show
  1. package/dist/index.js +517 -48
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -25210,6 +25210,7 @@ var init_schema = __esm({
25210
25210
  elevenlabs: "ELEVENLABS_API_KEY",
25211
25211
  runway: "RUNWAY_API_SECRET",
25212
25212
  kling: "KLING_API_KEY",
25213
+ fal: "FAL_KEY",
25213
25214
  imgbb: "IMGBB_API_KEY",
25214
25215
  replicate: "REPLICATE_API_TOKEN",
25215
25216
  xai: "XAI_API_KEY",
@@ -25610,6 +25611,70 @@ function buildEmptyRootHtml(opts) {
25610
25611
  </html>
25611
25612
  `;
25612
25613
  }
25614
+ function buildDesignMd(opts) {
25615
+ const { name, style } = opts;
25616
+ const intro = style ? `Visual identity for **${name}**, scaffolded from the **${style.name}** style (after ${style.designer}). Customise freely \u2014 this file is the single source of truth for every scene's palette, typography, and motion.` : `Visual identity for **${name}**. Fill the sections below before authoring any scene HTML or generating any backdrop. Pick a named style with \`vibe scene styles\` if you want a credible starting point.`;
25617
+ const moodLine = style ? `**Mood:** ${style.mood} \xB7 **Best for:** ${style.bestFor}` : `**Mood:** _(one line \u2014 what should the viewer FEEL?)_`;
25618
+ const palette = style ? `${style.palette.map((c) => `- \`${c}\``).join("\n")}
25619
+
25620
+ ${style.paletteNotes}` : `- _hex_ \u2014 primary
25621
+ - _hex_ \u2014 accent
25622
+
25623
+ _2\u20133 colours max. Declare explicit hex values; never name colours abstractly._`;
25624
+ const typography = style ? style.typography : `_One family, two weights. State the role of each (headline / label / body)._`;
25625
+ const composition = style ? style.composition : `_Grid? Centered? Layered? How does negative space behave?_`;
25626
+ const motion = style ? `${style.motion}
25627
+
25628
+ **GSAP signature:** ${style.gsapSignature}` : `_How fast? Snappy or fluid? Overshoot or precision?_
25629
+
25630
+ **GSAP signature:** _e.g. \`expo.out\`, \`sine.inOut\`, \`back.out(1.8)\`_`;
25631
+ const transition = style ? style.transition : `_Which Hyperframes shader matches the energy? (Cinematic Zoom, Cross-Warp Morph, Glitch, Domain Warp, \u2026)_`;
25632
+ const avoid = style ? style.avoid.map((a) => `- ${a}`).join("\n") : `- _anti-pattern 1_
25633
+ - _anti-pattern 2_
25634
+ - _anti-pattern 3_`;
25635
+ return `# ${name} \u2014 Design
25636
+
25637
+ > **Hard-gate.** This file defines the visual identity of every scene.
25638
+ > Author it before generating any HTML, backdrop image, or motion.
25639
+ > The Hyperframes \`hyperframes\` skill enforces this: scenes that
25640
+ > contradict DESIGN.md are rejected.
25641
+
25642
+ ${intro}
25643
+
25644
+ ## Style
25645
+
25646
+ ${moodLine}
25647
+
25648
+ ## Palette
25649
+
25650
+ ${palette}
25651
+
25652
+ ## Typography
25653
+
25654
+ ${typography}
25655
+
25656
+ ## Composition
25657
+
25658
+ ${composition}
25659
+
25660
+ ## Motion
25661
+
25662
+ ${motion}
25663
+
25664
+ ## Transition
25665
+
25666
+ ${transition}
25667
+
25668
+ ## What NOT to do
25669
+
25670
+ ${avoid}
25671
+
25672
+ ---
25673
+
25674
+ _Browse other named styles: \`vibe scene styles\`_
25675
+ ${style ? `_This file was seeded by \`vibe scene init --visual-style "${style.name}"\`._` : `_Seed this file from a named style: \`vibe scene init <dir> --visual-style "<name>"\`._`}
25676
+ `;
25677
+ }
25613
25678
  function buildProjectClaudeMd(name) {
25614
25679
  return `# ${name} \u2014 Scene Authoring Project
25615
25680
 
@@ -25617,6 +25682,16 @@ This project is **bilingual**: it works with both VibeFrame (\`vibe\`) and
25617
25682
  HeyGen Hyperframes (\`hyperframes\`). You can run either CLI inside this
25618
25683
  directory.
25619
25684
 
25685
+ ## Visual identity hard-gate
25686
+
25687
+ **Author \`DESIGN.md\` before any scene HTML.** It defines palette,
25688
+ typography, motion, and transition rules. Both the agent-driven path and
25689
+ the fallback emit reference it; scenes that contradict DESIGN.md are
25690
+ rejected by the Hyperframes \`hyperframes\` skill.
25691
+
25692
+ Browse named styles: \`vibe scene styles\`. Re-seed from one with
25693
+ \`vibe scene init . --visual-style "Swiss Pulse"\` (idempotent).
25694
+
25620
25695
  ## Skills \u2014 USE THESE FIRST
25621
25696
 
25622
25697
  **Always invoke the relevant skill before authoring scenes.** Skills encode
@@ -25625,14 +25700,23 @@ semantics, VibeFrame pipeline conventions) that are NOT in generic web docs.
25625
25700
 
25626
25701
  | Skill | Command | When to use |
25627
25702
  | ----------------- | ---------------- | ------------------------------------------------------------------------------------- |
25628
- | **vibe-scene** | \`/vibe-scene\` | Authoring / editing per-scene HTML in this project \u2014 preferred |
25629
- | **hyperframes** | \`/hyperframes\` | Fallback: direct HF authoring (install via \`npx skills add heygen-com/hyperframes\`) |
25703
+ | **hyperframes** | \`/hyperframes\` | Cinematic-quality composition \u2014 DESIGN.md hard-gate, named styles, motion principles |
25704
+ | **vibe-scene** | \`/vibe-scene\` | VibeFrame's authoring loop, AI assets, lint feedback, pipeline integration |
25630
25705
  | **gsap** | \`/gsap\` | GSAP tweens, timelines, easing |
25631
25706
 
25632
- If the skill is not available, follow the **Key Rules** below.
25707
+ Install the Hyperframes skills once per machine:
25708
+
25709
+ \`\`\`bash
25710
+ npx skills add heygen-com/hyperframes
25711
+ \`\`\`
25712
+
25713
+ Restart your agent session (or reload the skill list) after installing.
25714
+ If skills aren't available, follow the **Key Rules** below \u2014 they cover
25715
+ the framework-level minimum, not the cinematic craft layer.
25633
25716
 
25634
25717
  ## Project structure
25635
25718
 
25719
+ - \`DESIGN.md\` \u2014 visual identity contract (palette, type, motion, transitions)
25636
25720
  - \`index.html\` \u2014 root composition (timeline)
25637
25721
  - \`compositions/scene-*.html\` \u2014 per-scene HTML authored by you or the agent
25638
25722
  - \`assets/\` \u2014 shared media (narration audio, images, video)
@@ -25750,6 +25834,17 @@ async function scaffoldSceneProject(opts) {
25750
25834
  await writeFile4(claudePath, buildProjectClaudeMd(name), "utf-8");
25751
25835
  created.push(claudePath);
25752
25836
  }
25837
+ const designPath = resolve5(dir, "DESIGN.md");
25838
+ if (await pathExists(designPath)) {
25839
+ skipped.push(designPath);
25840
+ } else {
25841
+ await writeFile4(
25842
+ designPath,
25843
+ buildDesignMd({ name, style: opts.visualStyle }),
25844
+ "utf-8"
25845
+ );
25846
+ created.push(designPath);
25847
+ }
25753
25848
  const gitignorePath = resolve5(dir, ".gitignore");
25754
25849
  if (await pathExists(gitignorePath)) {
25755
25850
  skipped.push(gitignorePath);
@@ -25773,6 +25868,180 @@ var init_scene_project = __esm({
25773
25868
  }
25774
25869
  });
25775
25870
 
25871
+ // src/commands/_shared/visual-styles.ts
25872
+ function listVisualStyles() {
25873
+ return STYLES;
25874
+ }
25875
+ function getVisualStyle(query) {
25876
+ const q = query.trim().toLowerCase();
25877
+ return STYLES.find(
25878
+ (s) => s.name.toLowerCase() === q || s.slug === q
25879
+ );
25880
+ }
25881
+ function visualStyleNames() {
25882
+ return STYLES.map((s) => `"${s.name}"`).join(", ");
25883
+ }
25884
+ var STYLES;
25885
+ var init_visual_styles = __esm({
25886
+ "src/commands/_shared/visual-styles.ts"() {
25887
+ "use strict";
25888
+ STYLES = [
25889
+ {
25890
+ name: "Swiss Pulse",
25891
+ slug: "swiss-pulse",
25892
+ designer: "Josef M\xFCller-Brockmann",
25893
+ mood: "Clinical, precise",
25894
+ bestFor: "SaaS dashboards, developer tools, APIs, metrics",
25895
+ palette: ["#1a1a1a", "#ffffff", "#0066FF"],
25896
+ paletteNotes: "Black, white, ONE accent \u2014 electric blue (#0066FF) or amber (#FFB300). Never both accents at once.",
25897
+ typography: "Helvetica or Inter Bold for headlines, Regular for labels. Numbers dominate at 80\u2013120px.",
25898
+ composition: "Grid-locked. Every element snaps to an invisible 12-column grid. Hard cuts only \u2014 no decorative transitions.",
25899
+ motion: "Animated counters count up from 0. Entries are fast and snap into place. Nothing floats.",
25900
+ transition: "Cinematic Zoom or SDF Iris (precise, geometric)",
25901
+ gsapSignature: "expo.out, power4.out \u2014 fast arrivals, hard stops",
25902
+ avoid: [
25903
+ "Decorative transitions (fades, dissolves) \u2014 use hard cuts",
25904
+ "Two accent colours competing in one frame",
25905
+ "Off-grid placement or floating elements"
25906
+ ]
25907
+ },
25908
+ {
25909
+ name: "Velvet Standard",
25910
+ slug: "velvet-standard",
25911
+ designer: "Massimo Vignelli",
25912
+ mood: "Premium, timeless",
25913
+ bestFor: "Luxury products, enterprise software, keynotes, investor decks",
25914
+ palette: ["#000000", "#ffffff", "#1a237e"],
25915
+ paletteNotes: "Black, white, ONE rich accent \u2014 deep navy (#1a237e) or gold (#c9a84c).",
25916
+ typography: "Thin sans-serif, ALL CAPS, wide letter-spacing (0.15em+). Sequential reveals only.",
25917
+ composition: "Generous negative space. Symmetrical, centered, architectural precision. Nothing busy.",
25918
+ motion: "Slow, deliberate. Sequential reveals with long holds. No frantic motion.",
25919
+ transition: "Cross-Warp Morph (elegant, organic flow between scenes)",
25920
+ gsapSignature: "sine.inOut, power1 \u2014 nothing snaps, everything glides",
25921
+ avoid: [
25922
+ "Tight letter-spacing \u2014 kills the premium register",
25923
+ "Bouncy or elastic easings \u2014 too playful",
25924
+ "Multiple elements arriving at once \u2014 break sequence"
25925
+ ]
25926
+ },
25927
+ {
25928
+ name: "Deconstructed",
25929
+ slug: "deconstructed",
25930
+ designer: "Neville Brody",
25931
+ mood: "Industrial, raw",
25932
+ bestFor: "Tech news, developer launches, security products, punk-energy reveals",
25933
+ palette: ["#1a1a1a", "#D4501E", "#f0f0f0"],
25934
+ paletteNotes: "Dark grey (#1a1a1a), rust orange (#D4501E), raw white (#f0f0f0).",
25935
+ typography: "Type at angles, overlapping edges, escaping frames. Bold industrial weight.",
25936
+ composition: "Gritty textures \u2014 scan-line effects, glitch artifacts baked into the design.",
25937
+ motion: "Text SLAMS and SHATTERS. Letters scramble then snap to final position.",
25938
+ transition: "Glitch shader or Whip Pan (breaks the rules, feels aggressive)",
25939
+ gsapSignature: "back.out(2.5), steps(8), elastic.out(1.2, 0.4) \u2014 intentional irregularity",
25940
+ avoid: [
25941
+ "Polished, centered compositions \u2014 must feel raw",
25942
+ "Smooth fades \u2014 use glitch / scramble entries",
25943
+ "Soft easings \u2014 every motion lands hard"
25944
+ ]
25945
+ },
25946
+ {
25947
+ name: "Maximalist Type",
25948
+ slug: "maximalist-type",
25949
+ designer: "Paula Scher",
25950
+ mood: "Loud, kinetic",
25951
+ bestFor: "Big product launches, milestone announcements, high-energy hype videos",
25952
+ palette: ["#E63946", "#FFD60A", "#000000", "#ffffff"],
25953
+ paletteNotes: "Bold saturated: red (#E63946), yellow (#FFD60A), black, white \u2014 maximum contrast.",
25954
+ typography: "Text IS the visual. Overlapping type layers at different scales and angles, filling 50\u201380% of frame.",
25955
+ composition: "Text layered OVER footage \u2014 never empty backgrounds. 2\u20133 second rapid-fire scenes.",
25956
+ motion: "Everything is kinetic \u2014 slamming, sliding, scaling. No static moments.",
25957
+ transition: "Ridged Burn (explosive, dramatic, impossible to ignore)",
25958
+ gsapSignature: "expo.out, back.out(1.8) \u2014 fast arrivals, hard stops",
25959
+ avoid: [
25960
+ "Static moments \u2014 every frame must move",
25961
+ "Empty backgrounds \u2014 text and footage must layer",
25962
+ "Single typeface at a single size \u2014 must be layered"
25963
+ ]
25964
+ },
25965
+ {
25966
+ name: "Data Drift",
25967
+ slug: "data-drift",
25968
+ designer: "Refik Anadol",
25969
+ mood: "Futuristic, immersive",
25970
+ bestFor: "AI products, ML platforms, data companies, speculative tech",
25971
+ palette: ["#0a0a0a", "#7c3aed", "#06b6d4"],
25972
+ paletteNotes: "Iridescent \u2014 deep black (#0a0a0a), electric purple (#7c3aed), cyan (#06b6d4).",
25973
+ typography: "Thin futuristic sans-serif \u2014 floating, weightless, minimal text.",
25974
+ composition: "Fluid morphing compositions. Extreme scale shifts (micro \u2192 macro). Particles coalesce into numbers.",
25975
+ motion: "Light traces data paths through the frame. Smooth, continuous, organic \u2014 nothing hard.",
25976
+ transition: "Gravitational Lens or Domain Warp (otherworldly distortion)",
25977
+ gsapSignature: "sine.inOut, power2.out \u2014 smooth, continuous, organic",
25978
+ avoid: [
25979
+ "Hard cuts \u2014 break the immersion",
25980
+ "Heavy/bold typography \u2014 too grounded",
25981
+ "Static compositions \u2014 must feel like flow"
25982
+ ]
25983
+ },
25984
+ {
25985
+ name: "Soft Signal",
25986
+ slug: "soft-signal",
25987
+ designer: "Stefan Sagmeister",
25988
+ mood: "Intimate, warm",
25989
+ bestFor: "Wellness brands, personal stories, lifestyle products, human-centered apps",
25990
+ palette: ["#F5A623", "#FFF8EC", "#C4A3A3", "#8FAF8C"],
25991
+ paletteNotes: "Warm amber (#F5A623), cream (#FFF8EC), dusty rose (#C4A3A3), sage green (#8FAF8C).",
25992
+ typography: "Handwritten-style or humanist serif fonts. Personal, lowercase, delicate.",
25993
+ composition: "Close-up framing feel \u2014 single element fills the frame. Nothing feels corporate.",
25994
+ motion: "Slow drifts and floats, never snaps. Soft organic motion throughout.",
25995
+ transition: "Thermal Distortion (warm, flowing, like heat shimmer)",
25996
+ gsapSignature: "sine.inOut, power1.inOut \u2014 everything breathes",
25997
+ avoid: [
25998
+ "Sharp geometric layouts \u2014 break the warmth",
25999
+ "Hard easings or snaps \u2014 too clinical",
26000
+ "Cool tones (blue/green-blue) without warm balance"
26001
+ ]
26002
+ },
26003
+ {
26004
+ name: "Folk Frequency",
26005
+ slug: "folk-frequency",
26006
+ designer: "Eduardo Terrazas",
26007
+ mood: "Cultural, vivid",
26008
+ bestFor: "Consumer apps, food platforms, community products, festive launches",
26009
+ palette: ["#FF1493", "#0047AB", "#FFE000", "#009B77"],
26010
+ paletteNotes: "Vivid folk: hot pink (#FF1493), cobalt blue (#0047AB), sun yellow (#FFE000), emerald (#009B77).",
26011
+ typography: "Bold warm rounded type. Every frame feels handcrafted.",
26012
+ composition: "Pattern and repetition \u2014 folk art rhythm and density. Layered compositions with rich visual texture.",
26013
+ motion: "Colorful motion \u2014 elements bounce, pop, and spin into place with joy.",
26014
+ transition: "Swirl Vortex or Ripple Waves (hypnotic, celebratory)",
26015
+ gsapSignature: "back.out(1.6), elastic.out(1, 0.5) \u2014 overshoots feel intentional",
26016
+ avoid: [
26017
+ "Muted or monochrome palettes \u2014 kill the celebration",
26018
+ "Pure flat / minimal compositions \u2014 must feel layered",
26019
+ "Linear easings \u2014 motion should feel joyful"
26020
+ ]
26021
+ },
26022
+ {
26023
+ name: "Shadow Cut",
26024
+ slug: "shadow-cut",
26025
+ designer: "Hans Hillmann",
26026
+ mood: "Dark, cinematic",
26027
+ bestFor: "Security products, dramatic reveals, investigative content, intense launches",
26028
+ palette: ["#0a0a0a", "#3a3a3a", "#ffffff", "#C1121F"],
26029
+ paletteNotes: "Near-monochrome \u2014 deep blacks (#0a0a0a), cold greys (#3a3a3a), stark white + ONE accent (blood red #C1121F or toxic green #39FF14).",
26030
+ typography: "Sharp angular text like film noir title cards. Heavy contrast, no softness.",
26031
+ composition: "Heavy shadow \u2014 elements emerge from darkness. The reveal IS the narrative.",
26032
+ motion: "Slow creeping push-ins, dramatic scale reveals. Silence before the hit matters.",
26033
+ transition: "Domain Warp (dissolves reality before revealing the next scene)",
26034
+ gsapSignature: "power4.in for exits, power3.out for dramatic reveals \u2014 pause before the hit",
26035
+ avoid: [
26036
+ "Bright or saturated palettes \u2014 kill the cinematic mood",
26037
+ "Bouncy easings \u2014 break the tension",
26038
+ "Quick cuts \u2014 let the reveal breathe"
26039
+ ]
26040
+ }
26041
+ ];
26042
+ }
26043
+ });
26044
+
25776
26045
  // src/commands/_shared/scene-html-emit.ts
25777
26046
  function esc(text) {
25778
26047
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
@@ -25798,6 +26067,52 @@ function buildTranscriptTweens(transcript, targetSelector) {
25798
26067
  return `tl.fromTo('${sel}', { opacity: 0, y: 10 }, { opacity: 1, y: 0, duration: 0.18, ease: 'power2.out' }, ${start});`;
25799
26068
  }).join("\n ");
25800
26069
  }
26070
+ function crossfadeTweens(scope, dur) {
26071
+ return `tl.from('${scope}', { opacity: 0, duration: ${SCENE_OVERLAP_SECONDS}, ease: 'power2.out' }, 0);
26072
+ tl.to('${scope}', { opacity: 0, duration: ${SCENE_OVERLAP_SECONDS}, ease: 'power2.in' }, ${(dur - SCENE_OVERLAP_SECONDS).toFixed(2)});`;
26073
+ }
26074
+ function fitFontSize(text, baseMaxPx, minPx, comfortableChars) {
26075
+ const charCount = text.length;
26076
+ if (charCount <= comfortableChars) return baseMaxPx;
26077
+ const ratio = comfortableChars / charCount;
26078
+ return Math.max(minPx, Math.round(baseMaxPx * ratio));
26079
+ }
26080
+ function fitKineticFontSize(words) {
26081
+ const totalChars = words.join(" ").length;
26082
+ if (totalChars <= 18) return 180;
26083
+ if (totalChars <= 26) return 140;
26084
+ if (totalChars <= 38) return 110;
26085
+ if (totalChars <= 54) return 90;
26086
+ return 72;
26087
+ }
26088
+ function kenBurnsTween(scope, dur, endScale = 1.08) {
26089
+ return `tl.fromTo('${scope} .backdrop', { scale: 1.0 }, { scale: ${endScale}, duration: ${dur.toFixed(2)}, ease: 'none' }, 0);`;
26090
+ }
26091
+ function idleHeroPulse(scope, selector, idleStart, dur) {
26092
+ const cycleDur = 1.5;
26093
+ const idleEnd = dur - SCENE_OVERLAP_SECONDS;
26094
+ const window2 = idleEnd - idleStart;
26095
+ if (window2 < cycleDur) return "";
26096
+ const plays = Math.max(1, Math.floor(window2 / cycleDur));
26097
+ const repeat = plays - 1;
26098
+ return `tl.to('${scope} ${selector}', { scale: 1.04, y: -8, duration: ${cycleDur}, yoyo: true, repeat: ${repeat}, ease: 'sine.inOut' }, ${idleStart.toFixed(2)});`;
26099
+ }
26100
+ function kineticWordBobs(scope, wordCount, idleStart, dur) {
26101
+ const cycleDur = 1.8;
26102
+ const idleEnd = dur - SCENE_OVERLAP_SECONDS;
26103
+ const window2 = idleEnd - idleStart;
26104
+ if (window2 < cycleDur) return "";
26105
+ const plays = Math.max(1, Math.floor(window2 / cycleDur));
26106
+ const repeat = plays - 1;
26107
+ const lines = [];
26108
+ for (let i = 0; i < wordCount; i++) {
26109
+ const start = (idleStart + i * 0.18).toFixed(2);
26110
+ lines.push(
26111
+ `tl.to('${scope} #w-${i}', { y: -12, duration: ${cycleDur}, yoyo: true, repeat: ${repeat}, ease: 'sine.inOut' }, ${start});`
26112
+ );
26113
+ }
26114
+ return lines.join("\n ");
26115
+ }
25801
26116
  function buildPreset(input) {
25802
26117
  const id = input.id;
25803
26118
  const scope = `[data-composition-id="${id}"]`;
@@ -25834,7 +26149,8 @@ function buildPreset(input) {
25834
26149
  const captionInner = useWordSync ? renderTranscriptSpans(transcript) : esc(captionText);
25835
26150
  const wordCss = useWordSync ? `
25836
26151
  ${scope} .caption .word { display: inline-block; opacity: 0; }` : "";
25837
- const timeline = useWordSync ? buildTranscriptTweens(transcript, `${scope} .caption .word`) : `tl.from('${scope} .caption', { opacity: 0, y: 28, duration: 0.45, ease: 'power3.out' }, 0.05);`;
26152
+ const captionFontPx = fitFontSize(captionText, 56, 28, 60);
26153
+ const captionTween = useWordSync ? buildTranscriptTweens(transcript, `${scope} .caption .word`) : `tl.from('${scope} .caption', { opacity: 0, y: 28, duration: 0.45, ease: 'power3.out' }, 0.05);`;
25838
26154
  return {
25839
26155
  css: `${scope} {
25840
26156
  position: absolute; inset: 0; width: 100%; height: 100%;
@@ -25842,21 +26158,27 @@ function buildPreset(input) {
25842
26158
  color: #fff; overflow: hidden; background: #000;
25843
26159
  }
25844
26160
  ${backdrop}
26161
+ ${scope} .backdrop { transform-origin: center; }
25845
26162
  ${scope} .caption {
25846
26163
  position: absolute;
25847
26164
  left: 8%; right: 8%; bottom: 12%;
25848
26165
  text-align: center;
25849
- font-size: 56px;
26166
+ font-size: ${captionFontPx}px;
25850
26167
  font-weight: 700;
25851
26168
  line-height: 1.2;
26169
+ overflow-wrap: break-word;
25852
26170
  text-shadow: 0 4px 20px rgba(0,0,0,0.65);
25853
26171
  }${wordCss}`,
25854
26172
  body: `${backdropMarkup}
25855
26173
  <div class="caption" id="caption">${captionInner}</div>`,
25856
- timeline
26174
+ timeline: `${crossfadeTweens(scope, dur)}
26175
+ ${kenBurnsTween(scope, dur)}
26176
+ ${captionTween}
26177
+ ${idleHeroPulse(scope, "#caption", 1, dur)}`
25857
26178
  };
25858
26179
  }
25859
26180
  case "announcement": {
26181
+ const fontSize = fitFontSize(headline, 160, 72, 22);
25860
26182
  return {
25861
26183
  css: `${scope} {
25862
26184
  position: absolute; inset: 0; width: 100%; height: 100%;
@@ -25864,6 +26186,7 @@ function buildPreset(input) {
25864
26186
  color: #fff; overflow: hidden; background: #000;
25865
26187
  }
25866
26188
  ${backdrop}
26189
+ ${scope} .backdrop { transform-origin: center; }
25867
26190
  ${scope} .announce {
25868
26191
  position: absolute; inset: 0;
25869
26192
  display: flex; align-items: center; justify-content: center;
@@ -25872,10 +26195,11 @@ function buildPreset(input) {
25872
26195
  }
25873
26196
  ${scope} .announce h1 {
25874
26197
  margin: 0;
25875
- font-size: 160px;
26198
+ font-size: ${fontSize}px;
25876
26199
  font-weight: 900;
25877
26200
  letter-spacing: -4px;
25878
- line-height: 1;
26201
+ line-height: 1.05;
26202
+ overflow-wrap: break-word;
25879
26203
  background: linear-gradient(90deg, #8e2de2, #00c9ff);
25880
26204
  -webkit-background-clip: text; background-clip: text; color: transparent;
25881
26205
  text-shadow: 0 8px 40px rgba(142,45,226,0.35);
@@ -25885,8 +26209,12 @@ function buildPreset(input) {
25885
26209
  // Faster, snappier entrance and no tail fade-out — see the
25886
26210
  // matching note in the `simple` case above. The headline stays
25887
26211
  // on screen until the producer cuts to the next clip; the next
25888
- // scene's own fade-in handles the visual transition.
25889
- timeline: `tl.from('${scope} #headline', { opacity: 0, scale: 0.92, duration: 0.55, ease: 'power3.out' }, 0.05);`
26212
+ // scene's own fade-in handles the visual transition. Backdrop
26213
+ // Ken-Burns fills what used to be 80% dead static frame.
26214
+ timeline: `${crossfadeTweens(scope, dur)}
26215
+ ${kenBurnsTween(scope, dur)}
26216
+ tl.from('${scope} #headline', { opacity: 0, scale: 0.92, duration: 0.55, ease: 'power3.out' }, 0.05);
26217
+ ${idleHeroPulse(scope, "#headline", 1, dur)}`
25890
26218
  };
25891
26219
  }
25892
26220
  case "explainer": {
@@ -25898,6 +26226,7 @@ function buildPreset(input) {
25898
26226
  const wordCss = useWordSync ? `
25899
26227
  ${scope} #subtitle .word { display: inline-block; opacity: 0; }` : "";
25900
26228
  const subtitleTween = useWordSync ? buildTranscriptTweens(transcript, `${scope} #subtitle .word`) : sub ? `tl.from('${scope} #subtitle', { opacity: 0, y: 30, duration: 0.55, ease: 'power3.out' }, 0.55);` : "";
26229
+ const titleFontPx = fitFontSize(headline, 110, 56, 30);
25901
26230
  return {
25902
26231
  css: `${scope} {
25903
26232
  position: absolute; inset: 0; width: 100%; height: 100%;
@@ -25905,6 +26234,7 @@ function buildPreset(input) {
25905
26234
  color: #fff; overflow: hidden; background: #000;
25906
26235
  }
25907
26236
  ${backdrop}
26237
+ ${scope} .backdrop { transform-origin: center; }
25908
26238
  ${scope} .stage {
25909
26239
  position: absolute; inset: 0;
25910
26240
  display: flex; flex-direction: column; justify-content: center;
@@ -25915,11 +26245,13 @@ function buildPreset(input) {
25915
26245
  color: #00c9ff; font-weight: 600;
25916
26246
  }
25917
26247
  ${scope} .title {
25918
- font-size: 110px; font-weight: 800; letter-spacing: -2px;
26248
+ font-size: ${titleFontPx}px; font-weight: 800; letter-spacing: -2px;
25919
26249
  line-height: 1.05; margin: 0;
26250
+ overflow-wrap: break-word;
25920
26251
  }
25921
26252
  ${scope} .subtitle {
25922
26253
  font-size: 38px; font-weight: 300; color: #c0c0d0; max-width: 80%;
26254
+ overflow-wrap: break-word;
25923
26255
  }${wordCss}`,
25924
26256
  body: `${backdropMarkup}
25925
26257
  <div class="stage">
@@ -25927,9 +26259,12 @@ function buildPreset(input) {
25927
26259
  <h1 class="title" id="title">${esc(headline)}</h1>${sub ? `
25928
26260
  <div class="subtitle" id="subtitle">${subtitleInner}</div>` : ""}
25929
26261
  </div>`,
25930
- timeline: `tl.from('${scope} #kicker', { opacity: 0, y: 16, duration: 0.4, ease: 'power2.out' }, 0.1);
26262
+ timeline: `${crossfadeTweens(scope, dur)}
26263
+ ${kenBurnsTween(scope, dur)}
26264
+ tl.from('${scope} #kicker', { opacity: 0, y: 16, duration: 0.4, ease: 'power2.out' }, 0.1);
25931
26265
  tl.from('${scope} #title', { opacity: 0, y: 60, duration: 0.7, ease: 'power3.out' }, 0.25);
25932
- ${subtitleTween}`
26266
+ ${subtitleTween}
26267
+ ${idleHeroPulse(scope, "#title", 1.2, dur)}`
25933
26268
  };
25934
26269
  }
25935
26270
  case "kinetic-type": {
@@ -25945,6 +26280,9 @@ function buildPreset(input) {
25945
26280
  const start = (0.05 + i * stagger).toFixed(2);
25946
26281
  return `tl.from('${scope} #w-${i}', { opacity: 0, y: 80, scale: 0.8, duration: 0.45, ease: 'back.out(1.8)' }, ${start});`;
25947
26282
  }).join("\n ");
26283
+ const kineticFontPx = fitKineticFontSize(words);
26284
+ const lastWordEntryEnd = useWordSync ? Math.max(...transcript.map((w) => w.end + 0.2)) : 0.05 + (words.length - 1) * stagger + 0.45 + 0.2;
26285
+ const idleStart = Math.max(1, Number(lastWordEntryEnd.toFixed(2)));
25948
26286
  return {
25949
26287
  css: `${scope} {
25950
26288
  position: absolute; inset: 0; width: 100%; height: 100%;
@@ -25952,21 +26290,28 @@ function buildPreset(input) {
25952
26290
  color: #fff; overflow: hidden; background: #000;
25953
26291
  }
25954
26292
  ${backdrop}
26293
+ ${scope} .backdrop { transform-origin: center; }
25955
26294
  ${scope} .kinetic {
25956
26295
  position: absolute; inset: 0;
25957
26296
  display: flex; align-items: center; justify-content: center;
26297
+ flex-wrap: wrap; gap: 8px 16px;
25958
26298
  text-align: center; padding: 0 6%;
25959
- font-size: 180px; font-weight: 900; letter-spacing: -6px;
26299
+ font-size: ${kineticFontPx}px; font-weight: 900; letter-spacing: -6px;
25960
26300
  line-height: 1; text-shadow: 0 6px 30px rgba(0,0,0,0.6);
26301
+ overflow-wrap: break-word;
25961
26302
  }
25962
26303
  ${scope} .kinetic .word { display: inline-block; margin: 0 12px; }`,
25963
26304
  body: `${backdropMarkup}
25964
26305
  <div class="kinetic">${wordSpans}</div>`,
25965
- timeline: tweens
26306
+ timeline: `${crossfadeTweens(scope, dur)}
26307
+ ${kenBurnsTween(scope, dur)}
26308
+ ${tweens}
26309
+ ${kineticWordBobs(scope, words.length, idleStart, dur)}`
25966
26310
  };
25967
26311
  }
25968
26312
  case "product-shot": {
25969
26313
  const label = kicker || humanise(id);
26314
+ const headlineFontPx = fitFontSize(headline, 72, 40, 50);
25970
26315
  return {
25971
26316
  css: `${scope} {
25972
26317
  position: absolute; inset: 0; width: 100%; height: 100%;
@@ -25984,22 +26329,26 @@ function buildPreset(input) {
25984
26329
  }
25985
26330
  ${scope} .product-headline {
25986
26331
  position: absolute; left: 8%; right: 8%; bottom: 14%;
25987
- font-size: 72px; font-weight: 800; letter-spacing: -1px;
25988
- line-height: 1.1; text-shadow: 0 4px 20px rgba(0,0,0,0.7);
26332
+ font-size: ${headlineFontPx}px; font-weight: 800; letter-spacing: -1px;
26333
+ line-height: 1.1; overflow-wrap: break-word;
26334
+ text-shadow: 0 4px 20px rgba(0,0,0,0.7);
25989
26335
  }${subhead ? `
25990
26336
  ${scope} .product-sub {
25991
26337
  position: absolute; left: 8%; right: 8%; bottom: 8%;
25992
26338
  font-size: 28px; font-weight: 400; color: #d0d0e0;
26339
+ overflow-wrap: break-word;
25993
26340
  text-shadow: 0 2px 10px rgba(0,0,0,0.7);
25994
26341
  }` : ""}`,
25995
26342
  body: `${backdropMarkup}
25996
26343
  <div class="label" id="label">${esc(label)}</div>
25997
26344
  <div class="product-headline" id="headline">${esc(headline)}</div>${subhead ? `
25998
26345
  <div class="product-sub" id="subhead">${esc(subhead)}</div>` : ""}`,
25999
- timeline: `tl.fromTo('${scope} .backdrop', { scale: 1.0 }, { scale: 1.08, duration: ${dur.toFixed(2)}, ease: 'none' }, 0);
26346
+ timeline: `${crossfadeTweens(scope, dur)}
26347
+ tl.fromTo('${scope} .backdrop', { scale: 1.0 }, { scale: 1.12, duration: ${dur.toFixed(2)}, ease: 'none' }, 0);
26000
26348
  tl.from('${scope} #label', { opacity: 0, x: -30, duration: 0.5, ease: 'power3.out' }, 0.2);
26001
26349
  tl.from('${scope} #headline', { opacity: 0, y: 40, duration: 0.6, ease: 'power3.out' }, 0.4);${subhead ? `
26002
- tl.from('${scope} #subhead', { opacity: 0, y: 20, duration: 0.5, ease: 'power3.out' }, 0.65);` : ""}`
26350
+ tl.from('${scope} #subhead', { opacity: 0, y: 20, duration: 0.5, ease: 'power3.out' }, 0.65);` : ""}
26351
+ ${idleHeroPulse(scope, "#headline", 1.2, dur)}`
26003
26352
  };
26004
26353
  }
26005
26354
  }
@@ -26045,7 +26394,7 @@ ${audioBlock}
26045
26394
  </template>
26046
26395
  `;
26047
26396
  }
26048
- function nextSceneStart(rootHtml) {
26397
+ function nextSceneStart(rootHtml, overlap = 0) {
26049
26398
  const clipRegex = /<div\s+class="clip"[^>]*?\sdata-start="([\d.]+)"[^>]*?\sdata-duration="([\d.]+)"/gi;
26050
26399
  let maxEnd = 0;
26051
26400
  let match;
@@ -26053,14 +26402,15 @@ function nextSceneStart(rootHtml) {
26053
26402
  const end = parseFloat(match[1]) + parseFloat(match[2]);
26054
26403
  if (Number.isFinite(end) && end > maxEnd) maxEnd = end;
26055
26404
  }
26056
- return maxEnd;
26405
+ return Math.max(0, maxEnd - overlap);
26057
26406
  }
26058
26407
  function buildClipReference(opts) {
26059
26408
  const start = Number(opts.start.toFixed(3));
26060
26409
  const duration = Number(opts.duration.toFixed(3));
26061
26410
  const track = opts.trackIndex ?? 1;
26062
26411
  const src = opts.src ?? `compositions/scene-${opts.id}.html`;
26063
- return `<div class="clip" data-composition-id="${esc(opts.id)}" data-composition-src="${esc(src)}" data-start="${start}" data-duration="${duration}" data-track-index="${track}"></div>`;
26412
+ const zIndex = Math.max(1, 1e6 - Math.floor(start * 1e3));
26413
+ return `<div class="clip" data-composition-id="${esc(opts.id)}" data-composition-src="${esc(src)}" data-start="${start}" data-duration="${duration}" data-track-index="${track}" style="z-index: ${zIndex};"></div>`;
26064
26414
  }
26065
26415
  function insertClipIntoRoot(rootHtml, clip) {
26066
26416
  const clipDiv = buildClipReference(clip);
@@ -26093,7 +26443,7 @@ function readRootDims(rootHtml) {
26093
26443
  if (!widthMatch || !heightMatch) return null;
26094
26444
  return { width: parseInt(widthMatch[1], 10), height: parseInt(heightMatch[1], 10) };
26095
26445
  }
26096
- var SCENE_PRESETS, GSAP_CDN;
26446
+ var SCENE_PRESETS, GSAP_CDN, SCENE_OVERLAP_SECONDS;
26097
26447
  var init_scene_html_emit = __esm({
26098
26448
  "src/commands/_shared/scene-html-emit.ts"() {
26099
26449
  "use strict";
@@ -26105,6 +26455,7 @@ var init_scene_html_emit = __esm({
26105
26455
  "product-shot"
26106
26456
  ];
26107
26457
  GSAP_CDN = "https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js";
26458
+ SCENE_OVERLAP_SECONDS = 0.4;
26108
26459
  }
26109
26460
  });
26110
26461
 
@@ -26788,6 +27139,18 @@ function validatePreset(value) {
26788
27139
  }
26789
27140
  return value;
26790
27141
  }
27142
+ function validateVisualStyle(value) {
27143
+ const found = getVisualStyle(value);
27144
+ if (!found) {
27145
+ exitWithError(
27146
+ usageError(
27147
+ `Unknown visual style: ${value}`,
27148
+ `Valid: ${visualStyleNames()}. Browse details with \`vibe scene styles\`.`
27149
+ )
27150
+ );
27151
+ }
27152
+ return found;
27153
+ }
26791
27154
  async function pathExists2(p) {
26792
27155
  try {
26793
27156
  await access4(p);
@@ -27001,7 +27364,20 @@ async function executeSceneAdd(opts) {
27001
27364
  }
27002
27365
  const cfg = await loadVibeProjectConfig(projectDir);
27003
27366
  const fallback2 = cfg?.defaultSceneDuration ?? 5;
27004
- const duration = opts.duration ?? narrationDuration ?? fallback2;
27367
+ const NARRATION_TAIL_BUFFER = 0.5;
27368
+ const userDur = opts.duration;
27369
+ const audioMinDur = narrationDuration !== void 0 ? narrationDuration + SCENE_OVERLAP_SECONDS + NARRATION_TAIL_BUFFER : void 0;
27370
+ let duration;
27371
+ if (userDur !== void 0 && audioMinDur !== void 0) {
27372
+ duration = Math.max(userDur, audioMinDur);
27373
+ } else if (audioMinDur !== void 0) {
27374
+ duration = audioMinDur;
27375
+ } else if (userDur !== void 0) {
27376
+ duration = userDur;
27377
+ } else {
27378
+ duration = fallback2;
27379
+ }
27380
+ duration = Number(duration.toFixed(2));
27005
27381
  opts.onProgress?.("Emitting scene HTML...");
27006
27382
  const sceneHtml = emitSceneHtml({
27007
27383
  id,
@@ -27019,8 +27395,10 @@ async function executeSceneAdd(opts) {
27019
27395
  await mkdir4(dirname3(scenePath), { recursive: true });
27020
27396
  await writeFile6(scenePath, sceneHtml, "utf-8");
27021
27397
  opts.onProgress?.("Updating root composition...");
27022
- const start = nextSceneStart(rootHtmlBefore);
27023
- const updated = insertClipIntoRoot(rootHtmlBefore, { id, start, duration });
27398
+ const start = nextSceneStart(rootHtmlBefore, SCENE_OVERLAP_SECONDS);
27399
+ const existingClipCount = (rootHtmlBefore.match(/<div\s+class="clip"/g) || []).length;
27400
+ const trackIndex = existingClipCount % 2 + 1;
27401
+ const updated = insertClipIntoRoot(rootHtmlBefore, { id, start, duration, trackIndex });
27024
27402
  await writeFile6(rootPath, updated, "utf-8");
27025
27403
  const transcriptAbsPath = transcriptRelPath ? resolve10(projectDir, transcriptRelPath) : void 0;
27026
27404
  return {
@@ -27079,6 +27457,7 @@ var init_scene = __esm({
27079
27457
  init_dist2();
27080
27458
  init_tts_resolve();
27081
27459
  init_scene_project();
27460
+ init_visual_styles();
27082
27461
  init_scene_html_emit();
27083
27462
  init_scene_lint();
27084
27463
  init_scene_render();
@@ -27103,21 +27482,28 @@ Examples:
27103
27482
 
27104
27483
  A scene project is bilingual: it works with both \`vibe\` and \`npx hyperframes\`.
27105
27484
  Run 'vibe schema scene.<command>' for structured parameter info.`);
27106
- sceneCommand.command("init").description("Scaffold a new scene project (or safely augment an existing Hyperframes project)").argument("<dir>", "Project directory (created if it doesn't exist)").option("-n, --name <name>", "Project name (defaults to directory basename)").option("-r, --ratio <ratio>", "Aspect ratio: 16:9, 9:16, 1:1, 4:5", "16:9").option("-d, --duration <sec>", "Default root composition duration (seconds)", "10").option("--dry-run", "Preview parameters without writing files").action(async (dir, options) => {
27485
+ sceneCommand.command("init").description("Scaffold a new scene project (or safely augment an existing Hyperframes project)").argument("<dir>", "Project directory (created if it doesn't exist)").option("-n, --name <name>", "Project name (defaults to directory basename)").option("-r, --ratio <ratio>", "Aspect ratio: 16:9, 9:16, 1:1, 4:5", "16:9").option("-d, --duration <sec>", "Default root composition duration (seconds)", "10").option("--visual-style <name>", `Seed DESIGN.md from a named style (browse via \`vibe scene styles\`). E.g. "Swiss Pulse"`).option("--dry-run", "Preview parameters without writing files").action(async (dir, options) => {
27107
27486
  const aspect = validateAspect(options.ratio);
27108
27487
  const duration = validateDuration(options.duration);
27109
27488
  const name = options.name ?? basename4(dir.replace(/\/+$/, ""));
27489
+ const visualStyle = options.visualStyle ? validateVisualStyle(options.visualStyle) : void 0;
27110
27490
  if (options.dryRun) {
27111
27491
  outputResult({
27112
27492
  dryRun: true,
27113
27493
  command: "scene init",
27114
- params: { dir, name, aspect, duration }
27494
+ params: {
27495
+ dir,
27496
+ name,
27497
+ aspect,
27498
+ duration,
27499
+ visualStyle: visualStyle?.name ?? null
27500
+ }
27115
27501
  });
27116
27502
  return;
27117
27503
  }
27118
27504
  const spinner2 = isJsonMode() ? null : ora(`Scaffolding scene project at ${dir}...`).start();
27119
27505
  try {
27120
- const result = await scaffoldSceneProject({ dir, name, aspect, duration });
27506
+ const result = await scaffoldSceneProject({ dir, name, aspect, duration, visualStyle });
27121
27507
  if (isJsonMode()) {
27122
27508
  outputResult({
27123
27509
  success: true,
@@ -27126,6 +27512,7 @@ Run 'vibe schema scene.<command>' for structured parameter info.`);
27126
27512
  name,
27127
27513
  aspect,
27128
27514
  duration,
27515
+ visualStyle: visualStyle?.name ?? null,
27129
27516
  created: result.created,
27130
27517
  merged: result.merged,
27131
27518
  skipped: result.skipped
@@ -27142,7 +27529,13 @@ Run 'vibe schema scene.<command>' for structured parameter info.`);
27142
27529
  console.log();
27143
27530
  console.log(source_default.bold.cyan("Next steps"));
27144
27531
  console.log(source_default.dim("\u2500".repeat(60)));
27145
- console.log(` ${source_default.cyan("vibe scene add")} <name> ${source_default.dim("# author a scene via AI")}`);
27532
+ if (visualStyle) {
27533
+ console.log(` ${source_default.dim("DESIGN.md seeded with")} ${source_default.bold(visualStyle.name)} ${source_default.dim("\u2014 review and customise.")}`);
27534
+ } else {
27535
+ console.log(` ${source_default.cyan("vibe scene styles")} ${source_default.dim("# pick a named style for DESIGN.md")}`);
27536
+ }
27537
+ console.log(` ${source_default.cyan("npx skills add heygen-com/hyperframes")} ${source_default.dim("# load the cinematic-craft skill set")}`);
27538
+ console.log(` ${source_default.cyan("vibe scene add")} <name> ${source_default.dim("# fallback path: 5-preset emit")}`);
27146
27539
  console.log(` ${source_default.cyan("vibe scene lint")} ${source_default.dim("# validate HTML")}`);
27147
27540
  console.log(` ${source_default.cyan("vibe scene render")} ${source_default.dim("# render to MP4")}`);
27148
27541
  } catch (error) {
@@ -27151,6 +27544,69 @@ Run 'vibe schema scene.<command>' for structured parameter info.`);
27151
27544
  exitWithError(generalError(`Failed to scaffold: ${msg}`));
27152
27545
  }
27153
27546
  });
27547
+ sceneCommand.command("styles").description("List vendored visual styles (or show one) for DESIGN.md seeding").argument("[name]", "Style name to inspect (omit to list all)").action((name) => {
27548
+ if (!name) {
27549
+ const all = listVisualStyles();
27550
+ if (isJsonMode()) {
27551
+ outputResult({
27552
+ success: true,
27553
+ command: "scene styles",
27554
+ count: all.length,
27555
+ styles: all.map((s) => ({
27556
+ name: s.name,
27557
+ slug: s.slug,
27558
+ designer: s.designer,
27559
+ mood: s.mood,
27560
+ bestFor: s.bestFor
27561
+ }))
27562
+ });
27563
+ return;
27564
+ }
27565
+ console.log();
27566
+ console.log(source_default.bold.cyan("Visual styles"));
27567
+ console.log(source_default.dim("\u2500".repeat(60)));
27568
+ for (const s of all) {
27569
+ console.log(
27570
+ ` ${source_default.bold(s.name.padEnd(18))} ${source_default.dim(s.mood.padEnd(24))} ${source_default.dim(s.bestFor)}`
27571
+ );
27572
+ }
27573
+ console.log();
27574
+ console.log(source_default.dim("Show details: "), source_default.cyan('vibe scene styles "<name>"'));
27575
+ console.log(source_default.dim("Seed DESIGN.md:"), source_default.cyan('vibe scene init <dir> --visual-style "<name>"'));
27576
+ return;
27577
+ }
27578
+ const style = getVisualStyle(name);
27579
+ if (!style) {
27580
+ exitWithError(
27581
+ usageError(
27582
+ `Unknown visual style: ${name}`,
27583
+ `Valid: ${visualStyleNames()}.`
27584
+ )
27585
+ );
27586
+ return;
27587
+ }
27588
+ if (isJsonMode()) {
27589
+ outputResult({ success: true, command: "scene styles", style });
27590
+ return;
27591
+ }
27592
+ console.log();
27593
+ console.log(source_default.bold.cyan(style.name), source_default.dim(`\u2014 ${style.designer}`));
27594
+ console.log(source_default.dim("\u2500".repeat(60)));
27595
+ console.log(`${source_default.bold("Mood:")} ${style.mood}`);
27596
+ console.log(`${source_default.bold("Best for:")} ${style.bestFor}`);
27597
+ console.log(`${source_default.bold("Palette:")} ${style.palette.join(", ")}`);
27598
+ console.log(source_default.dim(" ") + style.paletteNotes);
27599
+ console.log(`${source_default.bold("Typography:")} ${style.typography}`);
27600
+ console.log(`${source_default.bold("Composition:")} ${style.composition}`);
27601
+ console.log(`${source_default.bold("Motion:")} ${style.motion}`);
27602
+ console.log(`${source_default.bold("GSAP:")} ${style.gsapSignature}`);
27603
+ console.log(`${source_default.bold("Transition:")} ${style.transition}`);
27604
+ console.log();
27605
+ console.log(source_default.bold("Avoid:"));
27606
+ for (const a of style.avoid) console.log(` ${source_default.red("\u2022")} ${a}`);
27607
+ console.log();
27608
+ console.log(source_default.dim("Seed DESIGN.md:"), source_default.cyan(`vibe scene init <dir> --visual-style "${style.name}"`));
27609
+ });
27154
27610
  sceneCommand.command("add").description("Add a new scene to a project: AI narration + image + per-scene HTML").argument("<name>", "Scene name (slugified into the composition id)").option("--style <preset>", `Style preset: ${SCENE_PRESETS.join(", ")}`, "simple").option("--narration <text>", "Narration text (or path to a .txt file). Drives TTS + scene duration.").option("--narration-file <path>", "Existing narration audio file (.wav/.mp3). Skips TTS \u2014 useful with hyperframes tts, Mac say, or other external tools.").option("-d, --duration <sec>", "Explicit scene duration in seconds (overrides narration audio)").option("--visuals <prompt>", "Image prompt \u2014 generates assets/scene-<id>.png via the configured image provider").option("--headline <text>", "Visible headline (defaults to the humanised scene name)").option("--kicker <text>", "Small label above the headline (explainer / product-shot)").option("--insert-into <path>", "Root composition file to update", "index.html").option("--project <dir>", "Project directory", ".").option("--image-provider <name>", "Image provider: gemini, openai", "gemini").option("--tts <provider>", "TTS provider: auto, elevenlabs, kokoro (default auto \u2014 picks ElevenLabs when key set, else Kokoro local)", "auto").option("--voice <id>", "Voice id (ElevenLabs name/id, or Kokoro id like af_heart, am_michael)").option("--no-audio", "Skip TTS even when --narration is provided (useful for tests/agent dry runs)").option("--no-image", "Skip image generation even when --visuals is provided").option("--no-transcribe", "Skip Whisper word-level transcribe step (no transcript-<id>.json emitted)").option("--transcribe-language <code>", "BCP-47 language code passed to Whisper (e.g. en, ko)").option("--force", "Overwrite an existing compositions/scene-<id>.html").option("--dry-run", "Preview parameters without writing files or calling APIs").action(async (name, options) => {
27155
27611
  if (options.style) options.style = validatePreset(options.style);
27156
27612
  if (options.duration !== void 0) options.duration = validateDuration(options.duration);
@@ -30821,7 +31277,12 @@ async function executeSegmentsToScenes(opts) {
30821
31277
  const dims = aspectToDims(aspect);
30822
31278
  const preset = opts.scenePreset ?? DEFAULT_PRESET;
30823
31279
  const projectName = opts.projectName ?? basename7(outputDir);
30824
- const totalDuration = opts.segments.reduce((sum, s) => sum + (s.duration || 0), 0) || 10;
31280
+ const totalDuration = (() => {
31281
+ const sum = opts.segments.reduce((acc, s) => acc + (s.duration || 0), 0);
31282
+ if (sum <= 0) return 10;
31283
+ const joins = Math.max(0, opts.segments.length - 1);
31284
+ return Math.max(0.1, sum - joins * SCENE_OVERLAP_SECONDS);
31285
+ })();
30825
31286
  const baseError = (error) => ({
30826
31287
  success: false,
30827
31288
  outputDir,
@@ -30894,9 +31355,10 @@ async function executeSegmentsToScenes(opts) {
30894
31355
  id,
30895
31356
  start: clipStart,
30896
31357
  duration,
31358
+ trackIndex: i % 2 + 1,
30897
31359
  src: `compositions/${id}.html`
30898
31360
  });
30899
- clipStart += duration;
31361
+ clipStart += duration - SCENE_OVERLAP_SECONDS;
30900
31362
  }
30901
31363
  await writeFile10(rootAbs, rootHtml, "utf-8");
30902
31364
  let lintResult;
@@ -57537,7 +57999,7 @@ init_source();
57537
57999
  // package.json
57538
58000
  var package_default = {
57539
58001
  name: "@vibeframe/cli",
57540
- version: "0.57.2",
58002
+ version: "0.58.0",
57541
58003
  description: "VibeFrame CLI - natural language video editing from the terminal",
57542
58004
  private: false,
57543
58005
  license: "MIT",
@@ -65245,25 +65707,25 @@ var AI_FEATURES = [
65245
65707
  {
65246
65708
  label: "Images",
65247
65709
  desc: "generate + edit",
65248
- defaultProvider: "Gemini Nano Banana",
65249
- alsoAvailable: "OpenAI GPT Image, Grok Imagine",
65250
- keys: [{ configKey: "google", envVar: "GOOGLE_API_KEY", name: "Google", url: "https://aistudio.google.com/apikey", what: "Gemini image generation + editing + video analysis" }],
65710
+ defaultProvider: "OpenAI gpt-image-2 (Artificial Analysis #1, since v0.56)",
65711
+ alsoAvailable: "Gemini Nano Banana, Grok Imagine",
65712
+ keys: [{ configKey: "openai", envVar: "OPENAI_API_KEY", name: "OpenAI", url: "https://platform.openai.com/api-keys", what: "gpt-image-2 image generation + editing (also Whisper, Agent)" }],
65251
65713
  tryCommand: 'vibe generate image "a sunset over mountains" -o test.png'
65252
65714
  },
65253
65715
  {
65254
65716
  label: "Videos",
65255
65717
  desc: "generate + extend",
65256
- defaultProvider: "Grok Imagine (native audio)",
65257
- alsoAvailable: "Kling, Runway Gen-4.5, Google Veo",
65258
- keys: [{ configKey: "xai", envVar: "XAI_API_KEY", name: "xAI", url: "https://console.x.ai", what: "Grok video generation with native audio" }],
65718
+ defaultProvider: "fal.ai Seedance 2.0 (Artificial Analysis #2 t2v + i2v, since v0.57)",
65719
+ alsoAvailable: "Grok Imagine, Kling, Runway Gen-4.5, Google Veo",
65720
+ keys: [{ configKey: "fal", envVar: "FAL_KEY", name: "fal.ai", url: "https://fal.ai/dashboard/keys", what: "ByteDance Seedance 2.0 text-to-video and image-to-video" }],
65259
65721
  tryCommand: 'vibe generate video "ocean waves" -o waves.mp4'
65260
65722
  },
65261
65723
  {
65262
65724
  label: "Audio",
65263
65725
  desc: "TTS, SFX, music, voice clone",
65264
- defaultProvider: "ElevenLabs",
65726
+ defaultProvider: "ElevenLabs (paid, premium quality) \u2014 falls back to local Kokoro when no key (free, since v0.54)",
65265
65727
  alsoAvailable: "Replicate MusicGen (music only)",
65266
- keys: [{ configKey: "elevenlabs", envVar: "ELEVENLABS_API_KEY", name: "ElevenLabs", url: "https://elevenlabs.io/app/settings/api-keys", what: "Text-to-speech, sound effects, music, voice cloning" }],
65728
+ keys: [{ configKey: "elevenlabs", envVar: "ELEVENLABS_API_KEY", name: "ElevenLabs", url: "https://elevenlabs.io/app/settings/api-keys", what: "Text-to-speech, sound effects, music, voice cloning (skip to use local Kokoro)" }],
65267
65729
  tryCommand: 'vibe generate speech "Hello world" -o hello.mp3'
65268
65730
  },
65269
65731
  {
@@ -65315,9 +65777,9 @@ async function runSetupWizard(fullSetup = false) {
65315
65777
  config2.llm.provider = "claude";
65316
65778
  const pipelineKeys = [
65317
65779
  { configKey: "anthropic", envVar: "ANTHROPIC_API_KEY", name: "Anthropic", url: "https://console.anthropic.com/settings/keys", what: "Claude \u2014 storyboard generation + reasoning" },
65318
- { configKey: "google", envVar: "GOOGLE_API_KEY", name: "Google", url: "https://aistudio.google.com/apikey", what: "Gemini \u2014 image generation + video analysis" },
65319
- { configKey: "elevenlabs", envVar: "ELEVENLABS_API_KEY", name: "ElevenLabs", url: "https://elevenlabs.io/app/settings/api-keys", what: "Text-to-speech narration + music" },
65320
- { configKey: "xai", envVar: "XAI_API_KEY", name: "xAI", url: "https://console.x.ai", what: "Grok \u2014 video generation with native audio" }
65780
+ { configKey: "openai", envVar: "OPENAI_API_KEY", name: "OpenAI", url: "https://platform.openai.com/api-keys", what: "gpt-image-2 image generation (default since v0.56) + Whisper word-level transcribe" },
65781
+ { configKey: "fal", envVar: "FAL_KEY", name: "fal.ai", url: "https://fal.ai/dashboard/keys", what: "Seedance 2.0 \u2014 video generation, default since v0.57" },
65782
+ { configKey: "elevenlabs", envVar: "ELEVENLABS_API_KEY", name: "ElevenLabs", url: "https://elevenlabs.io/app/settings/api-keys", what: "Text-to-speech narration + music (skip to use local Kokoro)" }
65321
65783
  ];
65322
65784
  console.log(source_default.dim("Step 2 of 2"));
65323
65785
  console.log(source_default.bold("Pipeline requires these API keys:"));
@@ -65448,11 +65910,12 @@ async function runCustomSetup(config2) {
65448
65910
  console.log(source_default.dim(" Press Enter to skip any provider you don't need."));
65449
65911
  console.log();
65450
65912
  const allProviders = [
65913
+ { key: "openai", name: "OpenAI", env: "OPENAI_API_KEY", desc: "gpt-image-2 image gen ($, default since v0.56), Whisper transcribe, Agent" },
65451
65914
  { key: "anthropic", name: "Anthropic", env: "ANTHROPIC_API_KEY", desc: "Claude \u2014 storyboard, color grade, reframe, Agent ($)" },
65452
- { key: "openai", name: "OpenAI", env: "OPENAI_API_KEY", desc: "Whisper (captions, jump-cut $), GPT Image ($), Agent" },
65915
+ { key: "fal", name: "fal.ai", env: "FAL_KEY", desc: "Seedance 2.0 video gen ($$, default since v0.57)" },
65453
65916
  { key: "google", name: "Google", env: "GOOGLE_API_KEY", desc: "Gemini \u2014 image gen (free tier), video analysis ($), Veo ($$)" },
65454
65917
  { key: "xai", name: "xAI", env: "XAI_API_KEY", desc: "Grok \u2014 video gen with audio ($$), image ($), Agent" },
65455
- { key: "elevenlabs", name: "ElevenLabs", env: "ELEVENLABS_API_KEY", desc: "TTS ($), SFX, music, voice clone, dubbing" },
65918
+ { key: "elevenlabs", name: "ElevenLabs", env: "ELEVENLABS_API_KEY", desc: "TTS ($), SFX, music, voice clone, dubbing \u2014 skip to use local Kokoro" },
65456
65919
  { key: "runway", name: "Runway", env: "RUNWAY_API_SECRET", desc: "Gen-4.5 video generation ($$)" },
65457
65920
  { key: "kling", name: "Kling", env: "KLING_API_KEY", desc: "v2.5/v3 video \u2014 std ($$) and pro ($$$) modes" },
65458
65921
  { key: "openrouter", name: "OpenRouter", env: "OPENROUTER_API_KEY", desc: "300+ models via one key \u2014 Agent only (pay per model)" },
@@ -65566,6 +66029,7 @@ async function showConfig() {
65566
66029
  { key: "openai", name: "OpenAI", env: "OPENAI_API_KEY" },
65567
66030
  { key: "google", name: "Google", env: "GOOGLE_API_KEY" },
65568
66031
  { key: "xai", name: "xAI", env: "XAI_API_KEY" },
66032
+ { key: "fal", name: "fal.ai", env: "FAL_KEY" },
65569
66033
  { key: "elevenlabs", name: "ElevenLabs", env: "ELEVENLABS_API_KEY" },
65570
66034
  { key: "runway", name: "Runway", env: "RUNWAY_API_SECRET" },
65571
66035
  { key: "kling", name: "Kling", env: "KLING_API_KEY" },
@@ -65642,9 +66106,14 @@ var COMMAND_KEY_MAP = {
65642
66106
  XAI_API_KEY: [
65643
66107
  "agent -p xai",
65644
66108
  "generate image -p grok",
65645
- "generate video",
66109
+ "generate video -p grok",
65646
66110
  "edit image -p grok"
65647
66111
  ],
66112
+ FAL_KEY: [
66113
+ "generate video -p fal (Seedance 2.0 \u2014 default since v0.57)",
66114
+ "generate video -p fal -m fast (lower-latency variant)",
66115
+ "generate video -p fal -i <image> (image-to-video)"
66116
+ ],
65648
66117
  ELEVENLABS_API_KEY: [
65649
66118
  "generate speech",
65650
66119
  "generate sound-effect",
@@ -65656,7 +66125,7 @@ var COMMAND_KEY_MAP = {
65656
66125
  KLING_API_KEY: ["generate video -p kling"],
65657
66126
  RUNWAY_API_SECRET: ["generate video -p runway"],
65658
66127
  REPLICATE_API_TOKEN: ["generate music -p replicate"],
65659
- IMGBB_API_KEY: ["generate video -p kling (image-to-video)"]
66128
+ IMGBB_API_KEY: ["generate video -p kling/fal (image-to-video upload host)"]
65660
66129
  };
65661
66130
  var FREE_COMMANDS = [
65662
66131
  "edit silence-cut",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibeframe/cli",
3
- "version": "0.57.2",
3
+ "version": "0.58.0",
4
4
  "description": "VibeFrame CLI - natural language video editing from the terminal",
5
5
  "private": false,
6
6
  "license": "MIT",
@@ -149,8 +149,8 @@
149
149
  "tsx": "^4.21.0",
150
150
  "typescript": "^5.3.3",
151
151
  "vitest": "^1.2.2",
152
- "@vibeframe/ai-providers": "0.57.2",
153
- "@vibeframe/core": "0.57.2"
152
+ "@vibeframe/ai-providers": "0.58.0",
153
+ "@vibeframe/core": "0.58.0"
154
154
  },
155
155
  "engines": {
156
156
  "node": ">=20"