@vibeframe/cli 0.108.0 → 0.108.1

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.
@@ -85,6 +85,25 @@ export declare function isEpsilonOverlapFinding(finding: LintFinding): boolean;
85
85
  * findings pass through unchanged.
86
86
  */
87
87
  export declare function filterSubCompFalsePositives(findings: LintFinding[], isSubComposition: boolean): LintFinding[];
88
+ /**
89
+ * VibeFrame-side finding: inner phase clips inside a sub-composition.
90
+ *
91
+ * The producer's screenshot render mode does NOT toggle the visibility of
92
+ * `.clip` elements INSIDE a sub-composition by their `data-start` window —
93
+ * a scene split into clip phases (0-10s, 10-26s) renders BOTH phases
94
+ * simultaneously and text stacks on top of itself. Root index.html clips
95
+ * are unaffected (their windows work); this rule only applies to
96
+ * sub-composition internals. Phase changes must instead be driven by GSAP
97
+ * (animate the old phase out with autoAlpha, the new one in) inside
98
+ * full-window clips.
99
+ */
100
+ export declare function findInternalPhaseClipFindings(html: string): LintFinding[];
101
+ /**
102
+ * Append VibeFrame-side findings for sub-composition HTML. Call at every
103
+ * lint site (batch composer, scene_submit, scene repair, project lint) so
104
+ * all authoring paths share the exact same contract.
105
+ */
106
+ export declare function withVibeframeSubCompFindings(findings: LintFinding[], html: string, isSubComposition: boolean): LintFinding[];
88
107
  /**
89
108
  * Apply mechanical `--fix` rewrites for known-safe finding codes. Returns the
90
109
  * (possibly unchanged) HTML and the list of codes that were fixed at least
@@ -12,6 +12,8 @@ export interface StoryboardValidationResult {
12
12
  beats: Beat[];
13
13
  issues: StoryboardValidationIssue[];
14
14
  }
15
+ /** Beats beyond this render as static, overstuffed scenes. */
16
+ export declare const MAX_RECOMMENDED_BEAT_SEC = 15;
15
17
  export declare function validateStoryboardMarkdown(markdown: string): StoryboardValidationResult;
16
18
  export declare function getStoryboardBeat(markdown: string, beatId: string): Beat | null;
17
19
  export declare function setStoryboardCue(markdown: string, opts: {
package/dist/index.js CHANGED
@@ -26662,6 +26662,8 @@ imageProvider: openai
26662
26662
 
26663
26663
  Edit these beats before running \`vibe build\`. Each beat starts with
26664
26664
  YAML cues that drive narration, backdrop generation, and timing.
26665
+ Pacing: keep beats 6-15 seconds (split longer ones); a 90s video should
26666
+ have 6-8 beats, not 3 long ones.
26665
26667
 
26666
26668
  ## Beat hook \u2014 Hook
26667
26669
 
@@ -27778,6 +27780,29 @@ function filterSubCompFalsePositives(findings, isSubComposition) {
27778
27780
  if (!isSubComposition) return noEpsilon;
27779
27781
  return noEpsilon.filter((f) => !SUB_COMP_FALSE_POSITIVES.has(f.code));
27780
27782
  }
27783
+ function findInternalPhaseClipFindings(html) {
27784
+ const findings = [];
27785
+ for (const tagMatch of html.matchAll(/<[a-z][a-z0-9-]*\s[^>]*>/gi)) {
27786
+ const tag = tagMatch[0];
27787
+ if (!/class="[^"]*\bclip\b[^"]*"/i.test(tag)) continue;
27788
+ const startAttr = tag.match(/data-start="([^"]+)"/i);
27789
+ if (!startAttr) continue;
27790
+ const start = Number(startAttr[1]);
27791
+ if (!Number.isFinite(start) || start === 0) continue;
27792
+ findings.push({
27793
+ code: "internal_phase_clip_unsupported",
27794
+ severity: "error",
27795
+ message: `Inner .clip has data-start="${startAttr[1]}". The renderer does not toggle internal clip visibility inside sub-compositions, so phased clips render ALL phases at once (overlapping text).`,
27796
+ fixHint: 'Give every inner .clip data-start="0" spanning the full beat, and drive phase changes with GSAP autoAlpha instead (animate the previous phase out, the next one in).',
27797
+ snippet: tag.slice(0, 120)
27798
+ });
27799
+ }
27800
+ return findings;
27801
+ }
27802
+ function withVibeframeSubCompFindings(findings, html, isSubComposition) {
27803
+ if (!isSubComposition) return findings;
27804
+ return [...findings, ...findInternalPhaseClipFindings(html)];
27805
+ }
27781
27806
  function applyMechanicalFixes(html, findings) {
27782
27807
  const fixedCodes = /* @__PURE__ */ new Set();
27783
27808
  let updated = html;
@@ -27823,7 +27848,11 @@ async function runProjectLint(opts) {
27823
27848
  source: "projectDir"
27824
27849
  };
27825
27850
  const raw = runHyperframeLint(prepared);
27826
- const findings = filterSubCompFalsePositives(raw.findings, isSub);
27851
+ const findings = withVibeframeSubCompFindings(
27852
+ filterSubCompFalsePositives(raw.findings, isSub),
27853
+ html,
27854
+ isSub
27855
+ );
27827
27856
  if (opts.fix && findings.length > 0) {
27828
27857
  const { html: nextHtml, fixedCodes } = applyMechanicalFixes(html, findings);
27829
27858
  if (fixedCodes.length > 0) {
@@ -27833,7 +27862,11 @@ async function runProjectLint(opts) {
27833
27862
  files.push({
27834
27863
  file: rel,
27835
27864
  isSubComposition: isSub,
27836
- findings: filterSubCompFalsePositives(reLinted.findings, isSub)
27865
+ findings: withVibeframeSubCompFindings(
27866
+ filterSubCompFalsePositives(reLinted.findings, isSub),
27867
+ nextHtml,
27868
+ isSub
27869
+ )
27837
27870
  });
27838
27871
  continue;
27839
27872
  }
@@ -29713,8 +29746,14 @@ ${finalDurationBullet}
29713
29746
  Animate the backdrop/media plane instead; let text enter briefly, then hold
29714
29747
  still at its final CSS position. Continuous transforms on text ancestors can
29715
29748
  create subpixel shimmer in screenshot-captured renders.
29716
- - Timed children inside the composition have \`class="clip"\` plus
29717
- \`data-start\`, \`data-duration\`, \`data-track-index\`.
29749
+ - Inner \`.clip\` elements carry \`data-start\`, \`data-duration\`,
29750
+ \`data-track-index\` \u2014 but **every inner clip MUST be full-window:
29751
+ \`data-start="0"\` spanning the whole beat.** The renderer does NOT toggle
29752
+ internal clip visibility inside sub-compositions, so "phase clips" with a
29753
+ non-zero \`data-start\` render ALL phases at once and text stacks on top of
29754
+ itself. For multi-phase beats, drive phase changes with GSAP \`autoAlpha\`
29755
+ inside full-window clips: animate phase A out (\`autoAlpha: 0\`), then
29756
+ phase B in, on the same timeline.
29718
29757
  - If \`assets/backdrop-${ctx.beat.id}.png\` exists, use that local file as the
29719
29758
  full-frame visual backdrop. The exact path string is
29720
29759
  \`assets/backdrop-${ctx.beat.id}.png\`; do NOT prefix it with \`../\` or \`./\`
@@ -29728,7 +29767,9 @@ ${finalDurationBullet}
29728
29767
  narration.
29729
29768
  - Keep a meaningful visual progression across the full beat duration: line
29730
29769
  tracing, gentle camera drift, staged labels, value cards, or parallax. Avoid
29731
- a completed static frame holding silently for most of the beat.
29770
+ a completed static frame holding silently for most of the beat. For longer
29771
+ beats, progress through content phases with GSAP autoAlpha (old phase out,
29772
+ new phase in) \u2014 never with inner clip windows.
29732
29773
  - Do not import external font or image URLs. Use the project DESIGN.md font
29733
29774
  choice when explicit; otherwise use \`Inter\`, which is available in the
29734
29775
  deterministic render font map.
@@ -29903,7 +29944,11 @@ function lintBeatHtml(html, beatId) {
29903
29944
  source: "projectDir"
29904
29945
  };
29905
29946
  const raw = runHyperframeLint2(prepared);
29906
- const findings = filterSubCompFalsePositives(raw.findings, true);
29947
+ const findings = withVibeframeSubCompFindings(
29948
+ filterSubCompFalsePositives(raw.findings, true),
29949
+ html,
29950
+ true
29951
+ );
29907
29952
  return {
29908
29953
  errorCount: findings.filter((f) => f.severity === "error").length,
29909
29954
  warningCount: findings.filter((f) => f.severity === "warning").length,
@@ -30338,7 +30383,8 @@ function buildInstructions(args) {
30338
30383
  lines.push(`2. Read \`DESIGN.md\` for project-specific palette, typography, motion signature.`);
30339
30384
  lines.push(`3. For each beat in the \`beats\` array below, author HTML at \`outputPath\` matching the \`userPrompt\`. The beat \`body\` carries the narrative + visual + animation intent; \`cues\` carries machine-readable per-beat overrides (narration, duration, backdrop, voice).`);
30340
30385
  lines.push(`3b. Use each beat's \`finalDurationSec\` (narration-synced) for \`data-duration\` and timeline anchors when present \u2014 NOT the storyboard \`duration\`, which is only the minimum. Scenes composed at the storyboard duration end early and render black tails.`);
30341
- lines.push(`3c. If your environment cannot write files (e.g. Claude Desktop / MCP-only hosts), author each beat's HTML and submit it with the \`scene_submit\` tool (beat id + html). It validates with the same Hyperframes lint and writes the file for you; on lint errors it returns the findings without writing \u2014 fix and resubmit.`);
30386
+ lines.push(`3c. Never give inner \`.clip\` elements a non-zero \`data-start\` \u2014 the renderer does not toggle internal clip visibility inside sub-compositions, so phased clips render all phases at once (overlapping text). Use full-window clips and GSAP autoAlpha phase transitions instead. Also keep beats 6-15s; split anything longer in the storyboard first.`);
30387
+ lines.push(`3d. If your environment cannot write files (e.g. Claude Desktop / MCP-only hosts), author each beat's HTML and submit it with the \`scene_submit\` tool (beat id + html). It validates with the same Hyperframes lint and writes the file for you; on lint errors it returns the findings without writing \u2014 fix and resubmit.`);
30342
30388
  if (args.beatCount > 1) {
30343
30389
  lines.push(`4. After authoring all ${args.beatCount} beat(s), run \`vibe scene lint --fix\` to validate. Fix any remaining errors by editing the HTML directly.`);
30344
30390
  } else if (args.filtered) {
@@ -30780,7 +30826,11 @@ function lintHtml(html, rel, isSub) {
30780
30826
  source: "projectDir"
30781
30827
  };
30782
30828
  const raw = runHyperframeLint3(prepared);
30783
- return filterSubCompFalsePositives(raw.findings, isSub);
30829
+ return withVibeframeSubCompFindings(
30830
+ filterSubCompFalsePositives(raw.findings, isSub),
30831
+ html,
30832
+ isSub
30833
+ );
30784
30834
  }
30785
30835
  function lintFilesToIssues(files) {
30786
30836
  const issues = [];
@@ -44130,6 +44180,16 @@ function validateStoryboardMarkdown(markdown) {
44130
44180
  }
44131
44181
  }
44132
44182
  }
44183
+ for (const beat of parsed.beats) {
44184
+ if (beat.duration !== void 0 && beat.duration > MAX_RECOMMENDED_BEAT_SEC) {
44185
+ issues.push({
44186
+ severity: "warning",
44187
+ code: "BEAT_DURATION_TOO_LONG",
44188
+ beatId: beat.id,
44189
+ message: `Beat "${beat.id}" is ${beat.duration}s \u2014 beats longer than ${MAX_RECOMMENDED_BEAT_SEC}s render static and overstuffed. Split it into 6-15s beats (a 90s video should have 6-8 beats).`
44190
+ });
44191
+ }
44192
+ }
44133
44193
  return {
44134
44194
  ok: !issues.some((i) => i.severity === "error"),
44135
44195
  beats: parsed.beats,
@@ -44228,7 +44288,7 @@ function readLeadingCueBlock(body) {
44228
44288
  return { full: match[0], error: error instanceof Error ? error.message : String(error) };
44229
44289
  }
44230
44290
  }
44231
- var import_yaml5, STORYBOARD_CUE_KEYS, HEADING_RE2, LEADING_CUE_RE, ALLOWED_CUE_KEYS, STRING_CUE_KEYS;
44291
+ var import_yaml5, STORYBOARD_CUE_KEYS, HEADING_RE2, LEADING_CUE_RE, ALLOWED_CUE_KEYS, STRING_CUE_KEYS, MAX_RECOMMENDED_BEAT_SEC;
44232
44292
  var init_storyboard_edit = __esm({
44233
44293
  "src/commands/_shared/storyboard-edit.ts"() {
44234
44294
  "use strict";
@@ -44248,6 +44308,7 @@ var init_storyboard_edit = __esm({
44248
44308
  LEADING_CUE_RE = /^(\s*)```ya?ml\s*\n([\s\S]*?)\n```\s*(?:\n|$)/;
44249
44309
  ALLOWED_CUE_KEYS = new Set(STORYBOARD_CUE_KEYS);
44250
44310
  STRING_CUE_KEYS = /* @__PURE__ */ new Set(["narration", "backdrop", "video", "motion", "voice", "music", "asset"]);
44311
+ MAX_RECOMMENDED_BEAT_SEC = 15;
44251
44312
  }
44252
44313
  });
44253
44314
 
@@ -59092,7 +59153,8 @@ function revisionMessages(opts) {
59092
59153
  "The JSON object must have: storyboardMd string, summary string, changedBeats string[], warnings string[].",
59093
59154
  "Preserve existing frontmatter, beat ids, cue YAML keys, and useful prose unless the user asks to change structure.",
59094
59155
  "Keep cue YAML valid. Allowed cue keys are duration, narration, backdrop, video, motion, voice, music, asset.",
59095
- "If a target duration is supplied, every beat must have a positive duration cue and durations must sum to that target."
59156
+ "If a target duration is supplied, every beat must have a positive duration cue and durations must sum to that target.",
59157
+ "Aim for 6-15 seconds per beat and never exceed ~15s \u2014 long beats render as static, overstuffed scenes. A 90-second video should have 6-8 beats."
59096
59158
  ].join("\n");
59097
59159
  const repairBlock = opts.invalidStoryboard ? [
59098
59160
  "The previous revision failed validation. Repair it.",
@@ -72828,7 +72890,7 @@ init_source();
72828
72890
  // package.json
72829
72891
  var package_default = {
72830
72892
  name: "@vibeframe/cli",
72831
- version: "0.108.0",
72893
+ version: "0.108.1",
72832
72894
  description: "VibeFrame CLI - agentic video workflow runtime",
72833
72895
  private: false,
72834
72896
  license: "MIT",
@@ -81287,6 +81349,9 @@ providers:
81287
81349
 
81288
81350
  # ${product} - Storyboard
81289
81351
 
81352
+ Pacing: keep beats 6-15 seconds. Longer beats render as static, overstuffed
81353
+ scenes \u2014 split them instead.
81354
+
81290
81355
  Brief: ${brief}
81291
81356
 
81292
81357
  ${beats.map(renderBeatDraft).join("\n\n")}
@@ -25266,6 +25266,8 @@ imageProvider: openai
25266
25266
 
25267
25267
  Edit these beats before running \`vibe build\`. Each beat starts with
25268
25268
  YAML cues that drive narration, backdrop generation, and timing.
25269
+ Pacing: keep beats 6-15 seconds (split longer ones); a 90s video should
25270
+ have 6-8 beats, not 3 long ones.
25269
25271
 
25270
25272
  ## Beat hook \u2014 Hook
25271
25273
 
@@ -28829,6 +28831,29 @@ function filterSubCompFalsePositives(findings, isSubComposition) {
28829
28831
  if (!isSubComposition) return noEpsilon;
28830
28832
  return noEpsilon.filter((f) => !SUB_COMP_FALSE_POSITIVES.has(f.code));
28831
28833
  }
28834
+ function findInternalPhaseClipFindings(html) {
28835
+ const findings = [];
28836
+ for (const tagMatch of html.matchAll(/<[a-z][a-z0-9-]*\s[^>]*>/gi)) {
28837
+ const tag = tagMatch[0];
28838
+ if (!/class="[^"]*\bclip\b[^"]*"/i.test(tag)) continue;
28839
+ const startAttr = tag.match(/data-start="([^"]+)"/i);
28840
+ if (!startAttr) continue;
28841
+ const start = Number(startAttr[1]);
28842
+ if (!Number.isFinite(start) || start === 0) continue;
28843
+ findings.push({
28844
+ code: "internal_phase_clip_unsupported",
28845
+ severity: "error",
28846
+ message: `Inner .clip has data-start="${startAttr[1]}". The renderer does not toggle internal clip visibility inside sub-compositions, so phased clips render ALL phases at once (overlapping text).`,
28847
+ fixHint: 'Give every inner .clip data-start="0" spanning the full beat, and drive phase changes with GSAP autoAlpha instead (animate the previous phase out, the next one in).',
28848
+ snippet: tag.slice(0, 120)
28849
+ });
28850
+ }
28851
+ return findings;
28852
+ }
28853
+ function withVibeframeSubCompFindings(findings, html, isSubComposition) {
28854
+ if (!isSubComposition) return findings;
28855
+ return [...findings, ...findInternalPhaseClipFindings(html)];
28856
+ }
28832
28857
  function applyMechanicalFixes(html, findings) {
28833
28858
  const fixedCodes = /* @__PURE__ */ new Set();
28834
28859
  let updated = html;
@@ -28874,7 +28899,11 @@ async function runProjectLint(opts) {
28874
28899
  source: "projectDir"
28875
28900
  };
28876
28901
  const raw = runHyperframeLint(prepared);
28877
- const findings = filterSubCompFalsePositives(raw.findings, isSub);
28902
+ const findings = withVibeframeSubCompFindings(
28903
+ filterSubCompFalsePositives(raw.findings, isSub),
28904
+ html,
28905
+ isSub
28906
+ );
28878
28907
  if (opts.fix && findings.length > 0) {
28879
28908
  const { html: nextHtml, fixedCodes } = applyMechanicalFixes(html, findings);
28880
28909
  if (fixedCodes.length > 0) {
@@ -28884,7 +28913,11 @@ async function runProjectLint(opts) {
28884
28913
  files.push({
28885
28914
  file: rel,
28886
28915
  isSubComposition: isSub,
28887
- findings: filterSubCompFalsePositives(reLinted.findings, isSub)
28916
+ findings: withVibeframeSubCompFindings(
28917
+ filterSubCompFalsePositives(reLinted.findings, isSub),
28918
+ nextHtml,
28919
+ isSub
28920
+ )
28888
28921
  });
28889
28922
  continue;
28890
28923
  }
@@ -30419,8 +30452,14 @@ ${finalDurationBullet}
30419
30452
  Animate the backdrop/media plane instead; let text enter briefly, then hold
30420
30453
  still at its final CSS position. Continuous transforms on text ancestors can
30421
30454
  create subpixel shimmer in screenshot-captured renders.
30422
- - Timed children inside the composition have \`class="clip"\` plus
30423
- \`data-start\`, \`data-duration\`, \`data-track-index\`.
30455
+ - Inner \`.clip\` elements carry \`data-start\`, \`data-duration\`,
30456
+ \`data-track-index\` \u2014 but **every inner clip MUST be full-window:
30457
+ \`data-start="0"\` spanning the whole beat.** The renderer does NOT toggle
30458
+ internal clip visibility inside sub-compositions, so "phase clips" with a
30459
+ non-zero \`data-start\` render ALL phases at once and text stacks on top of
30460
+ itself. For multi-phase beats, drive phase changes with GSAP \`autoAlpha\`
30461
+ inside full-window clips: animate phase A out (\`autoAlpha: 0\`), then
30462
+ phase B in, on the same timeline.
30424
30463
  - If \`assets/backdrop-${ctx.beat.id}.png\` exists, use that local file as the
30425
30464
  full-frame visual backdrop. The exact path string is
30426
30465
  \`assets/backdrop-${ctx.beat.id}.png\`; do NOT prefix it with \`../\` or \`./\`
@@ -30434,7 +30473,9 @@ ${finalDurationBullet}
30434
30473
  narration.
30435
30474
  - Keep a meaningful visual progression across the full beat duration: line
30436
30475
  tracing, gentle camera drift, staged labels, value cards, or parallax. Avoid
30437
- a completed static frame holding silently for most of the beat.
30476
+ a completed static frame holding silently for most of the beat. For longer
30477
+ beats, progress through content phases with GSAP autoAlpha (old phase out,
30478
+ new phase in) \u2014 never with inner clip windows.
30438
30479
  - Do not import external font or image URLs. Use the project DESIGN.md font
30439
30480
  choice when explicit; otherwise use \`Inter\`, which is available in the
30440
30481
  deterministic render font map.
@@ -30609,7 +30650,11 @@ function lintBeatHtml(html, beatId) {
30609
30650
  source: "projectDir"
30610
30651
  };
30611
30652
  const raw = runHyperframeLint2(prepared);
30612
- const findings = filterSubCompFalsePositives(raw.findings, true);
30653
+ const findings = withVibeframeSubCompFindings(
30654
+ filterSubCompFalsePositives(raw.findings, true),
30655
+ html,
30656
+ true
30657
+ );
30613
30658
  return {
30614
30659
  errorCount: findings.filter((f) => f.severity === "error").length,
30615
30660
  warningCount: findings.filter((f) => f.severity === "warning").length,
@@ -31044,7 +31089,8 @@ function buildInstructions(args) {
31044
31089
  lines.push(`2. Read \`DESIGN.md\` for project-specific palette, typography, motion signature.`);
31045
31090
  lines.push(`3. For each beat in the \`beats\` array below, author HTML at \`outputPath\` matching the \`userPrompt\`. The beat \`body\` carries the narrative + visual + animation intent; \`cues\` carries machine-readable per-beat overrides (narration, duration, backdrop, voice).`);
31046
31091
  lines.push(`3b. Use each beat's \`finalDurationSec\` (narration-synced) for \`data-duration\` and timeline anchors when present \u2014 NOT the storyboard \`duration\`, which is only the minimum. Scenes composed at the storyboard duration end early and render black tails.`);
31047
- lines.push(`3c. If your environment cannot write files (e.g. Claude Desktop / MCP-only hosts), author each beat's HTML and submit it with the \`scene_submit\` tool (beat id + html). It validates with the same Hyperframes lint and writes the file for you; on lint errors it returns the findings without writing \u2014 fix and resubmit.`);
31092
+ lines.push(`3c. Never give inner \`.clip\` elements a non-zero \`data-start\` \u2014 the renderer does not toggle internal clip visibility inside sub-compositions, so phased clips render all phases at once (overlapping text). Use full-window clips and GSAP autoAlpha phase transitions instead. Also keep beats 6-15s; split anything longer in the storyboard first.`);
31093
+ lines.push(`3d. If your environment cannot write files (e.g. Claude Desktop / MCP-only hosts), author each beat's HTML and submit it with the \`scene_submit\` tool (beat id + html). It validates with the same Hyperframes lint and writes the file for you; on lint errors it returns the findings without writing \u2014 fix and resubmit.`);
31048
31094
  if (args.beatCount > 1) {
31049
31095
  lines.push(`4. After authoring all ${args.beatCount} beat(s), run \`vibe scene lint --fix\` to validate. Fix any remaining errors by editing the HTML directly.`);
31050
31096
  } else if (args.filtered) {
@@ -31367,7 +31413,11 @@ function lintHtml(html, rel, isSub) {
31367
31413
  source: "projectDir"
31368
31414
  };
31369
31415
  const raw = runHyperframeLint3(prepared);
31370
- return filterSubCompFalsePositives(raw.findings, isSub);
31416
+ return withVibeframeSubCompFindings(
31417
+ filterSubCompFalsePositives(raw.findings, isSub),
31418
+ html,
31419
+ isSub
31420
+ );
31371
31421
  }
31372
31422
  function lintFilesToIssues(files) {
31373
31423
  const issues = [];
@@ -32440,6 +32490,16 @@ function validateStoryboardMarkdown(markdown) {
32440
32490
  }
32441
32491
  }
32442
32492
  }
32493
+ for (const beat of parsed.beats) {
32494
+ if (beat.duration !== void 0 && beat.duration > MAX_RECOMMENDED_BEAT_SEC) {
32495
+ issues.push({
32496
+ severity: "warning",
32497
+ code: "BEAT_DURATION_TOO_LONG",
32498
+ beatId: beat.id,
32499
+ message: `Beat "${beat.id}" is ${beat.duration}s \u2014 beats longer than ${MAX_RECOMMENDED_BEAT_SEC}s render static and overstuffed. Split it into 6-15s beats (a 90s video should have 6-8 beats).`
32500
+ });
32501
+ }
32502
+ }
32443
32503
  return {
32444
32504
  ok: !issues.some((i) => i.severity === "error"),
32445
32505
  beats: parsed.beats,
@@ -32538,7 +32598,7 @@ function readLeadingCueBlock(body) {
32538
32598
  return { full: match[0], error: error instanceof Error ? error.message : String(error) };
32539
32599
  }
32540
32600
  }
32541
- var import_yaml5, STORYBOARD_CUE_KEYS, HEADING_RE2, LEADING_CUE_RE, ALLOWED_CUE_KEYS, STRING_CUE_KEYS;
32601
+ var import_yaml5, STORYBOARD_CUE_KEYS, HEADING_RE2, LEADING_CUE_RE, ALLOWED_CUE_KEYS, STRING_CUE_KEYS, MAX_RECOMMENDED_BEAT_SEC;
32542
32602
  var init_storyboard_edit = __esm({
32543
32603
  "src/commands/_shared/storyboard-edit.ts"() {
32544
32604
  "use strict";
@@ -32558,6 +32618,7 @@ var init_storyboard_edit = __esm({
32558
32618
  LEADING_CUE_RE = /^(\s*)```ya?ml\s*\n([\s\S]*?)\n```\s*(?:\n|$)/;
32559
32619
  ALLOWED_CUE_KEYS = new Set(STORYBOARD_CUE_KEYS);
32560
32620
  STRING_CUE_KEYS = /* @__PURE__ */ new Set(["narration", "backdrop", "video", "motion", "voice", "music", "asset"]);
32621
+ MAX_RECOMMENDED_BEAT_SEC = 15;
32561
32622
  }
32562
32623
  });
32563
32624
 
@@ -56751,7 +56812,8 @@ function revisionMessages(opts) {
56751
56812
  "The JSON object must have: storyboardMd string, summary string, changedBeats string[], warnings string[].",
56752
56813
  "Preserve existing frontmatter, beat ids, cue YAML keys, and useful prose unless the user asks to change structure.",
56753
56814
  "Keep cue YAML valid. Allowed cue keys are duration, narration, backdrop, video, motion, voice, music, asset.",
56754
- "If a target duration is supplied, every beat must have a positive duration cue and durations must sum to that target."
56815
+ "If a target duration is supplied, every beat must have a positive duration cue and durations must sum to that target.",
56816
+ "Aim for 6-15 seconds per beat and never exceed ~15s \u2014 long beats render as static, overstuffed scenes. A 90-second video should have 6-8 beats."
56755
56817
  ].join("\n");
56756
56818
  const repairBlock = opts.invalidStoryboard ? [
56757
56819
  "The previous revision failed validation. Repair it.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibeframe/cli",
3
- "version": "0.108.0",
3
+ "version": "0.108.1",
4
4
  "description": "VibeFrame CLI - agentic video workflow runtime",
5
5
  "private": false,
6
6
  "license": "MIT",
@@ -83,8 +83,8 @@
83
83
  "tsx": "^4.21.0",
84
84
  "typescript": "^5.3.3",
85
85
  "vitest": "^1.2.2",
86
- "@vibeframe/ai-providers": "0.108.0",
87
- "@vibeframe/core": "0.108.0"
86
+ "@vibeframe/ai-providers": "0.108.1",
87
+ "@vibeframe/core": "0.108.1"
88
88
  },
89
89
  "engines": {
90
90
  "node": ">=20"