@vibeframe/cli 0.57.3 → 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 +494 -33
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -25611,6 +25611,70 @@ function buildEmptyRootHtml(opts) {
25611
25611
  </html>
25612
25612
  `;
25613
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
+ }
25614
25678
  function buildProjectClaudeMd(name) {
25615
25679
  return `# ${name} \u2014 Scene Authoring Project
25616
25680
 
@@ -25618,6 +25682,16 @@ This project is **bilingual**: it works with both VibeFrame (\`vibe\`) and
25618
25682
  HeyGen Hyperframes (\`hyperframes\`). You can run either CLI inside this
25619
25683
  directory.
25620
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
+
25621
25695
  ## Skills \u2014 USE THESE FIRST
25622
25696
 
25623
25697
  **Always invoke the relevant skill before authoring scenes.** Skills encode
@@ -25626,14 +25700,23 @@ semantics, VibeFrame pipeline conventions) that are NOT in generic web docs.
25626
25700
 
25627
25701
  | Skill | Command | When to use |
25628
25702
  | ----------------- | ---------------- | ------------------------------------------------------------------------------------- |
25629
- | **vibe-scene** | \`/vibe-scene\` | Authoring / editing per-scene HTML in this project \u2014 preferred |
25630
- | **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 |
25631
25705
  | **gsap** | \`/gsap\` | GSAP tweens, timelines, easing |
25632
25706
 
25633
- 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.
25634
25716
 
25635
25717
  ## Project structure
25636
25718
 
25719
+ - \`DESIGN.md\` \u2014 visual identity contract (palette, type, motion, transitions)
25637
25720
  - \`index.html\` \u2014 root composition (timeline)
25638
25721
  - \`compositions/scene-*.html\` \u2014 per-scene HTML authored by you or the agent
25639
25722
  - \`assets/\` \u2014 shared media (narration audio, images, video)
@@ -25751,6 +25834,17 @@ async function scaffoldSceneProject(opts) {
25751
25834
  await writeFile4(claudePath, buildProjectClaudeMd(name), "utf-8");
25752
25835
  created.push(claudePath);
25753
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
+ }
25754
25848
  const gitignorePath = resolve5(dir, ".gitignore");
25755
25849
  if (await pathExists(gitignorePath)) {
25756
25850
  skipped.push(gitignorePath);
@@ -25774,6 +25868,180 @@ var init_scene_project = __esm({
25774
25868
  }
25775
25869
  });
25776
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
+
25777
26045
  // src/commands/_shared/scene-html-emit.ts
25778
26046
  function esc(text) {
25779
26047
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
@@ -25799,6 +26067,52 @@ function buildTranscriptTweens(transcript, targetSelector) {
25799
26067
  return `tl.fromTo('${sel}', { opacity: 0, y: 10 }, { opacity: 1, y: 0, duration: 0.18, ease: 'power2.out' }, ${start});`;
25800
26068
  }).join("\n ");
25801
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
+ }
25802
26116
  function buildPreset(input) {
25803
26117
  const id = input.id;
25804
26118
  const scope = `[data-composition-id="${id}"]`;
@@ -25835,7 +26149,8 @@ function buildPreset(input) {
25835
26149
  const captionInner = useWordSync ? renderTranscriptSpans(transcript) : esc(captionText);
25836
26150
  const wordCss = useWordSync ? `
25837
26151
  ${scope} .caption .word { display: inline-block; opacity: 0; }` : "";
25838
- 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);`;
25839
26154
  return {
25840
26155
  css: `${scope} {
25841
26156
  position: absolute; inset: 0; width: 100%; height: 100%;
@@ -25843,21 +26158,27 @@ function buildPreset(input) {
25843
26158
  color: #fff; overflow: hidden; background: #000;
25844
26159
  }
25845
26160
  ${backdrop}
26161
+ ${scope} .backdrop { transform-origin: center; }
25846
26162
  ${scope} .caption {
25847
26163
  position: absolute;
25848
26164
  left: 8%; right: 8%; bottom: 12%;
25849
26165
  text-align: center;
25850
- font-size: 56px;
26166
+ font-size: ${captionFontPx}px;
25851
26167
  font-weight: 700;
25852
26168
  line-height: 1.2;
26169
+ overflow-wrap: break-word;
25853
26170
  text-shadow: 0 4px 20px rgba(0,0,0,0.65);
25854
26171
  }${wordCss}`,
25855
26172
  body: `${backdropMarkup}
25856
26173
  <div class="caption" id="caption">${captionInner}</div>`,
25857
- timeline
26174
+ timeline: `${crossfadeTweens(scope, dur)}
26175
+ ${kenBurnsTween(scope, dur)}
26176
+ ${captionTween}
26177
+ ${idleHeroPulse(scope, "#caption", 1, dur)}`
25858
26178
  };
25859
26179
  }
25860
26180
  case "announcement": {
26181
+ const fontSize = fitFontSize(headline, 160, 72, 22);
25861
26182
  return {
25862
26183
  css: `${scope} {
25863
26184
  position: absolute; inset: 0; width: 100%; height: 100%;
@@ -25865,6 +26186,7 @@ function buildPreset(input) {
25865
26186
  color: #fff; overflow: hidden; background: #000;
25866
26187
  }
25867
26188
  ${backdrop}
26189
+ ${scope} .backdrop { transform-origin: center; }
25868
26190
  ${scope} .announce {
25869
26191
  position: absolute; inset: 0;
25870
26192
  display: flex; align-items: center; justify-content: center;
@@ -25873,10 +26195,11 @@ function buildPreset(input) {
25873
26195
  }
25874
26196
  ${scope} .announce h1 {
25875
26197
  margin: 0;
25876
- font-size: 160px;
26198
+ font-size: ${fontSize}px;
25877
26199
  font-weight: 900;
25878
26200
  letter-spacing: -4px;
25879
- line-height: 1;
26201
+ line-height: 1.05;
26202
+ overflow-wrap: break-word;
25880
26203
  background: linear-gradient(90deg, #8e2de2, #00c9ff);
25881
26204
  -webkit-background-clip: text; background-clip: text; color: transparent;
25882
26205
  text-shadow: 0 8px 40px rgba(142,45,226,0.35);
@@ -25886,8 +26209,12 @@ function buildPreset(input) {
25886
26209
  // Faster, snappier entrance and no tail fade-out — see the
25887
26210
  // matching note in the `simple` case above. The headline stays
25888
26211
  // on screen until the producer cuts to the next clip; the next
25889
- // scene's own fade-in handles the visual transition.
25890
- 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)}`
25891
26218
  };
25892
26219
  }
25893
26220
  case "explainer": {
@@ -25899,6 +26226,7 @@ function buildPreset(input) {
25899
26226
  const wordCss = useWordSync ? `
25900
26227
  ${scope} #subtitle .word { display: inline-block; opacity: 0; }` : "";
25901
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);
25902
26230
  return {
25903
26231
  css: `${scope} {
25904
26232
  position: absolute; inset: 0; width: 100%; height: 100%;
@@ -25906,6 +26234,7 @@ function buildPreset(input) {
25906
26234
  color: #fff; overflow: hidden; background: #000;
25907
26235
  }
25908
26236
  ${backdrop}
26237
+ ${scope} .backdrop { transform-origin: center; }
25909
26238
  ${scope} .stage {
25910
26239
  position: absolute; inset: 0;
25911
26240
  display: flex; flex-direction: column; justify-content: center;
@@ -25916,11 +26245,13 @@ function buildPreset(input) {
25916
26245
  color: #00c9ff; font-weight: 600;
25917
26246
  }
25918
26247
  ${scope} .title {
25919
- font-size: 110px; font-weight: 800; letter-spacing: -2px;
26248
+ font-size: ${titleFontPx}px; font-weight: 800; letter-spacing: -2px;
25920
26249
  line-height: 1.05; margin: 0;
26250
+ overflow-wrap: break-word;
25921
26251
  }
25922
26252
  ${scope} .subtitle {
25923
26253
  font-size: 38px; font-weight: 300; color: #c0c0d0; max-width: 80%;
26254
+ overflow-wrap: break-word;
25924
26255
  }${wordCss}`,
25925
26256
  body: `${backdropMarkup}
25926
26257
  <div class="stage">
@@ -25928,9 +26259,12 @@ function buildPreset(input) {
25928
26259
  <h1 class="title" id="title">${esc(headline)}</h1>${sub ? `
25929
26260
  <div class="subtitle" id="subtitle">${subtitleInner}</div>` : ""}
25930
26261
  </div>`,
25931
- 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);
25932
26265
  tl.from('${scope} #title', { opacity: 0, y: 60, duration: 0.7, ease: 'power3.out' }, 0.25);
25933
- ${subtitleTween}`
26266
+ ${subtitleTween}
26267
+ ${idleHeroPulse(scope, "#title", 1.2, dur)}`
25934
26268
  };
25935
26269
  }
25936
26270
  case "kinetic-type": {
@@ -25946,6 +26280,9 @@ function buildPreset(input) {
25946
26280
  const start = (0.05 + i * stagger).toFixed(2);
25947
26281
  return `tl.from('${scope} #w-${i}', { opacity: 0, y: 80, scale: 0.8, duration: 0.45, ease: 'back.out(1.8)' }, ${start});`;
25948
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)));
25949
26286
  return {
25950
26287
  css: `${scope} {
25951
26288
  position: absolute; inset: 0; width: 100%; height: 100%;
@@ -25953,21 +26290,28 @@ function buildPreset(input) {
25953
26290
  color: #fff; overflow: hidden; background: #000;
25954
26291
  }
25955
26292
  ${backdrop}
26293
+ ${scope} .backdrop { transform-origin: center; }
25956
26294
  ${scope} .kinetic {
25957
26295
  position: absolute; inset: 0;
25958
26296
  display: flex; align-items: center; justify-content: center;
26297
+ flex-wrap: wrap; gap: 8px 16px;
25959
26298
  text-align: center; padding: 0 6%;
25960
- font-size: 180px; font-weight: 900; letter-spacing: -6px;
26299
+ font-size: ${kineticFontPx}px; font-weight: 900; letter-spacing: -6px;
25961
26300
  line-height: 1; text-shadow: 0 6px 30px rgba(0,0,0,0.6);
26301
+ overflow-wrap: break-word;
25962
26302
  }
25963
26303
  ${scope} .kinetic .word { display: inline-block; margin: 0 12px; }`,
25964
26304
  body: `${backdropMarkup}
25965
26305
  <div class="kinetic">${wordSpans}</div>`,
25966
- timeline: tweens
26306
+ timeline: `${crossfadeTweens(scope, dur)}
26307
+ ${kenBurnsTween(scope, dur)}
26308
+ ${tweens}
26309
+ ${kineticWordBobs(scope, words.length, idleStart, dur)}`
25967
26310
  };
25968
26311
  }
25969
26312
  case "product-shot": {
25970
26313
  const label = kicker || humanise(id);
26314
+ const headlineFontPx = fitFontSize(headline, 72, 40, 50);
25971
26315
  return {
25972
26316
  css: `${scope} {
25973
26317
  position: absolute; inset: 0; width: 100%; height: 100%;
@@ -25985,22 +26329,26 @@ function buildPreset(input) {
25985
26329
  }
25986
26330
  ${scope} .product-headline {
25987
26331
  position: absolute; left: 8%; right: 8%; bottom: 14%;
25988
- font-size: 72px; font-weight: 800; letter-spacing: -1px;
25989
- 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);
25990
26335
  }${subhead ? `
25991
26336
  ${scope} .product-sub {
25992
26337
  position: absolute; left: 8%; right: 8%; bottom: 8%;
25993
26338
  font-size: 28px; font-weight: 400; color: #d0d0e0;
26339
+ overflow-wrap: break-word;
25994
26340
  text-shadow: 0 2px 10px rgba(0,0,0,0.7);
25995
26341
  }` : ""}`,
25996
26342
  body: `${backdropMarkup}
25997
26343
  <div class="label" id="label">${esc(label)}</div>
25998
26344
  <div class="product-headline" id="headline">${esc(headline)}</div>${subhead ? `
25999
26345
  <div class="product-sub" id="subhead">${esc(subhead)}</div>` : ""}`,
26000
- 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);
26001
26348
  tl.from('${scope} #label', { opacity: 0, x: -30, duration: 0.5, ease: 'power3.out' }, 0.2);
26002
26349
  tl.from('${scope} #headline', { opacity: 0, y: 40, duration: 0.6, ease: 'power3.out' }, 0.4);${subhead ? `
26003
- 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)}`
26004
26352
  };
26005
26353
  }
26006
26354
  }
@@ -26046,7 +26394,7 @@ ${audioBlock}
26046
26394
  </template>
26047
26395
  `;
26048
26396
  }
26049
- function nextSceneStart(rootHtml) {
26397
+ function nextSceneStart(rootHtml, overlap = 0) {
26050
26398
  const clipRegex = /<div\s+class="clip"[^>]*?\sdata-start="([\d.]+)"[^>]*?\sdata-duration="([\d.]+)"/gi;
26051
26399
  let maxEnd = 0;
26052
26400
  let match;
@@ -26054,14 +26402,15 @@ function nextSceneStart(rootHtml) {
26054
26402
  const end = parseFloat(match[1]) + parseFloat(match[2]);
26055
26403
  if (Number.isFinite(end) && end > maxEnd) maxEnd = end;
26056
26404
  }
26057
- return maxEnd;
26405
+ return Math.max(0, maxEnd - overlap);
26058
26406
  }
26059
26407
  function buildClipReference(opts) {
26060
26408
  const start = Number(opts.start.toFixed(3));
26061
26409
  const duration = Number(opts.duration.toFixed(3));
26062
26410
  const track = opts.trackIndex ?? 1;
26063
26411
  const src = opts.src ?? `compositions/scene-${opts.id}.html`;
26064
- 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>`;
26065
26414
  }
26066
26415
  function insertClipIntoRoot(rootHtml, clip) {
26067
26416
  const clipDiv = buildClipReference(clip);
@@ -26094,7 +26443,7 @@ function readRootDims(rootHtml) {
26094
26443
  if (!widthMatch || !heightMatch) return null;
26095
26444
  return { width: parseInt(widthMatch[1], 10), height: parseInt(heightMatch[1], 10) };
26096
26445
  }
26097
- var SCENE_PRESETS, GSAP_CDN;
26446
+ var SCENE_PRESETS, GSAP_CDN, SCENE_OVERLAP_SECONDS;
26098
26447
  var init_scene_html_emit = __esm({
26099
26448
  "src/commands/_shared/scene-html-emit.ts"() {
26100
26449
  "use strict";
@@ -26106,6 +26455,7 @@ var init_scene_html_emit = __esm({
26106
26455
  "product-shot"
26107
26456
  ];
26108
26457
  GSAP_CDN = "https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js";
26458
+ SCENE_OVERLAP_SECONDS = 0.4;
26109
26459
  }
26110
26460
  });
26111
26461
 
@@ -26789,6 +27139,18 @@ function validatePreset(value) {
26789
27139
  }
26790
27140
  return value;
26791
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
+ }
26792
27154
  async function pathExists2(p) {
26793
27155
  try {
26794
27156
  await access4(p);
@@ -27002,7 +27364,20 @@ async function executeSceneAdd(opts) {
27002
27364
  }
27003
27365
  const cfg = await loadVibeProjectConfig(projectDir);
27004
27366
  const fallback2 = cfg?.defaultSceneDuration ?? 5;
27005
- 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));
27006
27381
  opts.onProgress?.("Emitting scene HTML...");
27007
27382
  const sceneHtml = emitSceneHtml({
27008
27383
  id,
@@ -27020,8 +27395,10 @@ async function executeSceneAdd(opts) {
27020
27395
  await mkdir4(dirname3(scenePath), { recursive: true });
27021
27396
  await writeFile6(scenePath, sceneHtml, "utf-8");
27022
27397
  opts.onProgress?.("Updating root composition...");
27023
- const start = nextSceneStart(rootHtmlBefore);
27024
- 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 });
27025
27402
  await writeFile6(rootPath, updated, "utf-8");
27026
27403
  const transcriptAbsPath = transcriptRelPath ? resolve10(projectDir, transcriptRelPath) : void 0;
27027
27404
  return {
@@ -27080,6 +27457,7 @@ var init_scene = __esm({
27080
27457
  init_dist2();
27081
27458
  init_tts_resolve();
27082
27459
  init_scene_project();
27460
+ init_visual_styles();
27083
27461
  init_scene_html_emit();
27084
27462
  init_scene_lint();
27085
27463
  init_scene_render();
@@ -27104,21 +27482,28 @@ Examples:
27104
27482
 
27105
27483
  A scene project is bilingual: it works with both \`vibe\` and \`npx hyperframes\`.
27106
27484
  Run 'vibe schema scene.<command>' for structured parameter info.`);
27107
- 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) => {
27108
27486
  const aspect = validateAspect(options.ratio);
27109
27487
  const duration = validateDuration(options.duration);
27110
27488
  const name = options.name ?? basename4(dir.replace(/\/+$/, ""));
27489
+ const visualStyle = options.visualStyle ? validateVisualStyle(options.visualStyle) : void 0;
27111
27490
  if (options.dryRun) {
27112
27491
  outputResult({
27113
27492
  dryRun: true,
27114
27493
  command: "scene init",
27115
- params: { dir, name, aspect, duration }
27494
+ params: {
27495
+ dir,
27496
+ name,
27497
+ aspect,
27498
+ duration,
27499
+ visualStyle: visualStyle?.name ?? null
27500
+ }
27116
27501
  });
27117
27502
  return;
27118
27503
  }
27119
27504
  const spinner2 = isJsonMode() ? null : ora(`Scaffolding scene project at ${dir}...`).start();
27120
27505
  try {
27121
- const result = await scaffoldSceneProject({ dir, name, aspect, duration });
27506
+ const result = await scaffoldSceneProject({ dir, name, aspect, duration, visualStyle });
27122
27507
  if (isJsonMode()) {
27123
27508
  outputResult({
27124
27509
  success: true,
@@ -27127,6 +27512,7 @@ Run 'vibe schema scene.<command>' for structured parameter info.`);
27127
27512
  name,
27128
27513
  aspect,
27129
27514
  duration,
27515
+ visualStyle: visualStyle?.name ?? null,
27130
27516
  created: result.created,
27131
27517
  merged: result.merged,
27132
27518
  skipped: result.skipped
@@ -27143,7 +27529,13 @@ Run 'vibe schema scene.<command>' for structured parameter info.`);
27143
27529
  console.log();
27144
27530
  console.log(source_default.bold.cyan("Next steps"));
27145
27531
  console.log(source_default.dim("\u2500".repeat(60)));
27146
- 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")}`);
27147
27539
  console.log(` ${source_default.cyan("vibe scene lint")} ${source_default.dim("# validate HTML")}`);
27148
27540
  console.log(` ${source_default.cyan("vibe scene render")} ${source_default.dim("# render to MP4")}`);
27149
27541
  } catch (error) {
@@ -27152,6 +27544,69 @@ Run 'vibe schema scene.<command>' for structured parameter info.`);
27152
27544
  exitWithError(generalError(`Failed to scaffold: ${msg}`));
27153
27545
  }
27154
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
+ });
27155
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) => {
27156
27611
  if (options.style) options.style = validatePreset(options.style);
27157
27612
  if (options.duration !== void 0) options.duration = validateDuration(options.duration);
@@ -30822,7 +31277,12 @@ async function executeSegmentsToScenes(opts) {
30822
31277
  const dims = aspectToDims(aspect);
30823
31278
  const preset = opts.scenePreset ?? DEFAULT_PRESET;
30824
31279
  const projectName = opts.projectName ?? basename7(outputDir);
30825
- 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
+ })();
30826
31286
  const baseError = (error) => ({
30827
31287
  success: false,
30828
31288
  outputDir,
@@ -30895,9 +31355,10 @@ async function executeSegmentsToScenes(opts) {
30895
31355
  id,
30896
31356
  start: clipStart,
30897
31357
  duration,
31358
+ trackIndex: i % 2 + 1,
30898
31359
  src: `compositions/${id}.html`
30899
31360
  });
30900
- clipStart += duration;
31361
+ clipStart += duration - SCENE_OVERLAP_SECONDS;
30901
31362
  }
30902
31363
  await writeFile10(rootAbs, rootHtml, "utf-8");
30903
31364
  let lintResult;
@@ -57538,7 +57999,7 @@ init_source();
57538
57999
  // package.json
57539
58000
  var package_default = {
57540
58001
  name: "@vibeframe/cli",
57541
- version: "0.57.3",
58002
+ version: "0.58.0",
57542
58003
  description: "VibeFrame CLI - natural language video editing from the terminal",
57543
58004
  private: false,
57544
58005
  license: "MIT",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibeframe/cli",
3
- "version": "0.57.3",
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.3",
153
- "@vibeframe/core": "0.57.3"
152
+ "@vibeframe/ai-providers": "0.58.0",
153
+ "@vibeframe/core": "0.58.0"
154
154
  },
155
155
  "engines": {
156
156
  "node": ">=20"