hyperframes 0.7.3 → 0.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -50,7 +50,7 @@ var VERSION;
50
50
  var init_version = __esm({
51
51
  "src/version.ts"() {
52
52
  "use strict";
53
- VERSION = true ? "0.7.3" : "0.0.0-dev";
53
+ VERSION = true ? "0.7.5" : "0.0.0-dev";
54
54
  }
55
55
  });
56
56
 
@@ -25071,8 +25071,16 @@ function findHtmlTag(source) {
25071
25071
  };
25072
25072
  }
25073
25073
  function findRootTag(source) {
25074
- const bodyOpenMatch = /<body\b[^>]*>/i.exec(source);
25074
+ const bodyOpenMatch = /<body\b([^>]*)>/i.exec(source);
25075
25075
  const bodyCloseMatch = /<\/body>/i.exec(source);
25076
+ if (bodyOpenMatch && (readAttr(bodyOpenMatch[0], "data-composition-id") || readAttr(bodyOpenMatch[0], "data-width") || readAttr(bodyOpenMatch[0], "data-height"))) {
25077
+ return {
25078
+ raw: bodyOpenMatch[0],
25079
+ name: "body",
25080
+ attrs: bodyOpenMatch[1] ?? "",
25081
+ index: bodyOpenMatch.index
25082
+ };
25083
+ }
25076
25084
  const bodyStart = bodyOpenMatch ? bodyOpenMatch.index + bodyOpenMatch[0].length : 0;
25077
25085
  const bodyEnd = bodyOpenMatch && bodyCloseMatch && bodyCloseMatch.index > bodyStart ? bodyCloseMatch.index : source.length;
25078
25086
  const bodyContent = bodyOpenMatch ? source.slice(bodyStart, bodyEnd) : source;
@@ -26137,6 +26145,30 @@ function isHiddenGsapState(values) {
26137
26145
  const display = stringValue(values.display)?.toLowerCase();
26138
26146
  return zeroValue(values.opacity) || zeroValue(values.autoAlpha) || visibility === "hidden" || display === "none";
26139
26147
  }
26148
+ function oneValue(values, keys2) {
26149
+ for (const key2 of keys2) {
26150
+ const value = values[key2];
26151
+ if (value !== void 0) return value;
26152
+ }
26153
+ return void 0;
26154
+ }
26155
+ function isVisibleGsapState(values) {
26156
+ const opacity = oneValue(values, ["opacity", "autoAlpha"]);
26157
+ if (typeof opacity === "number") return opacity > 0;
26158
+ if (typeof opacity === "string" && opacity.trim()) {
26159
+ const numeric = Number(opacity);
26160
+ if (Number.isFinite(numeric)) return numeric > 0;
26161
+ }
26162
+ const visibility = stringValue(values.visibility)?.toLowerCase();
26163
+ if (visibility === "visible" || visibility === "inherit") return true;
26164
+ const display = stringValue(values.display)?.toLowerCase();
26165
+ if (display && display !== "none") return true;
26166
+ return false;
26167
+ }
26168
+ function makesOverlayVisible(win) {
26169
+ if (win.method === "from" && isHiddenGsapState(win.propertyValues)) return true;
26170
+ return isVisibleGsapState(win.propertyValues);
26171
+ }
26140
26172
  function isSceneBoundaryExit(win) {
26141
26173
  if (win.end <= win.position) return false;
26142
26174
  if (win.method !== "to" && win.method !== "fromTo") return false;
@@ -26225,6 +26257,67 @@ function getSingleClassSelector(selector) {
26225
26257
  const match = selector.trim().match(/^\.(?<name>[A-Za-z0-9_-]+)$/);
26226
26258
  return match?.groups?.name || null;
26227
26259
  }
26260
+ function readStyleProperty(style, property) {
26261
+ const escapedProperty = property.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
26262
+ const match = style.match(new RegExp(`(?:^|;)\\s*${escapedProperty}\\s*:\\s*([^;]+)`, "i"));
26263
+ return match?.[1]?.trim() || null;
26264
+ }
26265
+ function cssZero(value) {
26266
+ if (!value) return false;
26267
+ return /^0(?:\.0+)?(?:px|%|vw|vh|rem|em)?$/i.test(value.trim());
26268
+ }
26269
+ function styleHasHiddenInitialState(style) {
26270
+ const opacity = readStyleProperty(style, "opacity");
26271
+ if (opacity && Number(opacity) === 0) return true;
26272
+ if (readStyleProperty(style, "visibility")?.toLowerCase() === "hidden") return true;
26273
+ if (readStyleProperty(style, "display")?.toLowerCase() === "none") return true;
26274
+ return false;
26275
+ }
26276
+ function styleHasOpaqueBackground(style) {
26277
+ const background = readStyleProperty(style, "background") || readStyleProperty(style, "background-color");
26278
+ if (!background) return false;
26279
+ const normalized = background.toLowerCase().replace(/\s+/g, "");
26280
+ if (normalized === "transparent" || normalized === "none") return false;
26281
+ if (/rgba?\([^)]*,0(?:\.0+)?\)$/.test(normalized)) return false;
26282
+ if (/hsla?\([^)]*,0(?:\.0+)?\)$/.test(normalized)) return false;
26283
+ return true;
26284
+ }
26285
+ function styleLooksFullFrameOverlay(style) {
26286
+ const position = readStyleProperty(style, "position")?.toLowerCase();
26287
+ if (position !== "fixed" && position !== "absolute") return false;
26288
+ const coversFrame = cssZero(readStyleProperty(style, "inset")) || cssZero(readStyleProperty(style, "top")) && cssZero(readStyleProperty(style, "right")) && cssZero(readStyleProperty(style, "bottom")) && cssZero(readStyleProperty(style, "left"));
26289
+ return coversFrame && styleHasOpaqueBackground(style);
26290
+ }
26291
+ function collectSimpleStyleRules(styles) {
26292
+ const rules = /* @__PURE__ */ new Map();
26293
+ for (const style of styles) {
26294
+ for (const [, selectorList, body] of style.content.matchAll(/([^{}]+)\{([^}]+)\}/g)) {
26295
+ if (!selectorList || !body) continue;
26296
+ for (const selector of selectorList.split(",")) {
26297
+ const token = selector.trim();
26298
+ if (!/^[#.][A-Za-z0-9_-]+$/.test(token)) continue;
26299
+ rules.set(token, `${rules.get(token) || ""};${body}`);
26300
+ }
26301
+ }
26302
+ }
26303
+ return rules;
26304
+ }
26305
+ function tagSimpleSelectors(tag) {
26306
+ const selectors = [];
26307
+ const id = readAttr(tag.raw, "id");
26308
+ if (id) selectors.push(`#${id}`);
26309
+ const classes = readAttr(tag.raw, "class")?.split(/\s+/).filter(Boolean) ?? [];
26310
+ for (const className of classes) selectors.push(`.${className}`);
26311
+ return selectors;
26312
+ }
26313
+ function combinedTagStyle(tag, styleRules) {
26314
+ const styles = [readAttr(tag.raw, "style") || ""];
26315
+ for (const selector of tagSimpleSelectors(tag)) {
26316
+ const ruleStyle = styleRules.get(selector);
26317
+ if (ruleStyle) styles.push(ruleStyle);
26318
+ }
26319
+ return styles.filter(Boolean).join(";");
26320
+ }
26228
26321
  function cssTransformToGsapProps(cssTransform) {
26229
26322
  const parts = [];
26230
26323
  const translateMatch = cssTransform.match(
@@ -26253,17 +26346,53 @@ function cssTransformToGsapProps(cssTransform) {
26253
26346
  }
26254
26347
  return parts.length > 0 ? parts.join(", ") : null;
26255
26348
  }
26256
- var SCENE_BOUNDARY_EPSILON_SECONDS, gsapWindowsCache, gsapRules;
26349
+ function targetedSelectorTokens(selector) {
26350
+ const tokens = /* @__PURE__ */ new Set();
26351
+ for (const group of selector.split(",")) {
26352
+ const compounds = group.trim().split(/[\s>+~]+/).filter(Boolean);
26353
+ const last = compounds[compounds.length - 1];
26354
+ if (!last) continue;
26355
+ const simple2 = last.match(/[#.][A-Za-z0-9_-]+/g);
26356
+ if (simple2) for (const token of simple2) tokens.add(token);
26357
+ }
26358
+ return tokens;
26359
+ }
26360
+ function matchCssTransform(gsapSelector, cssMap) {
26361
+ if (cssMap.size === 0) return void 0;
26362
+ const direct = cssMap.get(gsapSelector);
26363
+ if (direct) return direct;
26364
+ const tokens = targetedSelectorTokens(gsapSelector);
26365
+ for (const [cssSelector, value] of cssMap) {
26366
+ if (tokens.has(cssSelector)) return value;
26367
+ }
26368
+ return void 0;
26369
+ }
26370
+ function extractStandaloneGsapTransformCalls(script) {
26371
+ const calls = [];
26372
+ const pattern = /gsap\.(set|to|from|fromTo)\s*\(\s*(["'])([^"']+)\2\s*,\s*\{([^{}]*)\}/g;
26373
+ let match;
26374
+ while ((match = pattern.exec(script)) !== null) {
26375
+ const method = match[1] ?? "set";
26376
+ const selector = match[3] ?? "";
26377
+ const propsBody = match[4] ?? "";
26378
+ const properties = [...propsBody.matchAll(/([A-Za-z_$][\w$]*)\s*:/g)].map((m2) => m2[1] ?? "");
26379
+ calls.push({ method, selector, properties, raw: truncateSnippet(match[0]) ?? match[0] });
26380
+ }
26381
+ return calls;
26382
+ }
26383
+ var SCENE_BOUNDARY_EPSILON_SECONDS, gsapWindowsCache, CONFLICTING_TRANSLATE_PROPS, CONFLICTING_SCALE_PROPS, gsapRules;
26257
26384
  var init_gsap = __esm({
26258
26385
  "../core/src/lint/rules/gsap.ts"() {
26259
26386
  "use strict";
26260
26387
  init_utils2();
26261
26388
  SCENE_BOUNDARY_EPSILON_SECONDS = 0.05;
26262
26389
  gsapWindowsCache = /* @__PURE__ */ new Map();
26390
+ CONFLICTING_TRANSLATE_PROPS = ["x", "y", "xPercent", "yPercent"];
26391
+ CONFLICTING_SCALE_PROPS = ["scale", "scaleX", "scaleY"];
26263
26392
  gsapRules = [
26264
26393
  // overlapping_gsap_tweens + gsap_animates_clip_element + unscoped_gsap_selector
26265
26394
  // fallow-ignore-next-line complexity
26266
- async ({ source, tags, scripts, rootCompositionId }) => {
26395
+ async ({ source, tags, scripts, styles, rootCompositionId }) => {
26267
26396
  const findings = [];
26268
26397
  const clipIds = /* @__PURE__ */ new Map();
26269
26398
  const clipClasses = /* @__PURE__ */ new Map();
@@ -26284,6 +26413,8 @@ var init_gsap = __esm({
26284
26413
  }
26285
26414
  const classUsage = countClassUsage(tags);
26286
26415
  const clipStartBoundariesByComposition = collectClipStartBoundariesByComposition(source, tags);
26416
+ const styleRules = collectSimpleStyleRules(styles);
26417
+ const reportedVisibleOverlayKeys = /* @__PURE__ */ new Set();
26287
26418
  for (const script of scripts) {
26288
26419
  const localTimelineCompId = readRegisteredTimelineCompositionId(script.content);
26289
26420
  const gsapWindows = await cachedExtractGsapWindows(script.content);
@@ -26335,6 +26466,45 @@ ${right.raw}`)
26335
26466
  });
26336
26467
  }
26337
26468
  }
26469
+ for (const tag of tags) {
26470
+ const selectors = tagSimpleSelectors(tag);
26471
+ if (selectors.length === 0) continue;
26472
+ const overlayKey = readAttr(tag.raw, "id") || String(tag.index);
26473
+ if (reportedVisibleOverlayKeys.has(overlayKey)) continue;
26474
+ const authoredStyle = combinedTagStyle(tag, styleRules);
26475
+ if (!authoredStyle || !styleLooksFullFrameOverlay(authoredStyle)) continue;
26476
+ if (styleHasHiddenInitialState(authoredStyle)) continue;
26477
+ const visibilityWindows = gsapWindows.filter((win) => {
26478
+ const tokens = targetedSelectorTokens(win.targetSelector);
26479
+ if (!selectors.some((selector2) => tokens.has(selector2))) return false;
26480
+ return win.properties.some(
26481
+ (prop2) => ["opacity", "autoAlpha", "visibility", "display"].includes(prop2)
26482
+ );
26483
+ }).sort((a, b2) => a.position - b2.position);
26484
+ const startsHiddenAtZero = visibilityWindows.some(
26485
+ (win) => win.position <= SCENE_BOUNDARY_EPSILON_SECONDS && isHiddenGsapState(win.propertyValues)
26486
+ );
26487
+ if (startsHiddenAtZero) continue;
26488
+ const firstVisible = visibilityWindows.find((win) => makesOverlayVisible(win));
26489
+ if (!firstVisible) continue;
26490
+ const selector = selectors.find(
26491
+ (candidate) => targetedSelectorTokens(firstVisible.targetSelector).has(candidate)
26492
+ ) || selectors[0] || tag.name;
26493
+ const laterHidden = visibilityWindows.some(
26494
+ (win) => win.position >= firstVisible.position && isHiddenGsapState(win.propertyValues)
26495
+ );
26496
+ if (firstVisible.method !== "from" && !laterHidden) continue;
26497
+ reportedVisibleOverlayKeys.add(overlayKey);
26498
+ findings.push({
26499
+ code: "gsap_fullscreen_overlay_starts_visible",
26500
+ severity: "error",
26501
+ message: `Full-frame overlay "${selector}" starts visible before its first GSAP opacity tween at ${firstVisible.position.toFixed(2)}s. It will cover earlier render frames, often as a blank/white video.`,
26502
+ selector,
26503
+ elementId: readAttr(tag.raw, "id") || void 0,
26504
+ fixHint: `Add \`opacity: 0\` to "${selector}" in CSS/inline styles, or add \`tl.set("${selector}", { opacity: 0 }, 0)\` before the reveal tween.`,
26505
+ snippet: truncateSnippet(firstVisible.raw)
26506
+ });
26507
+ }
26338
26508
  for (const win of gsapWindows) {
26339
26509
  const sel = win.targetSelector;
26340
26510
  const clipInfo = clipIds.get(sel) || clipClasses.get(sel);
@@ -26413,22 +26583,30 @@ ${right.raw}`)
26413
26583
  for (const script of scripts) {
26414
26584
  if (!/gsap\.timeline/.test(script.content)) continue;
26415
26585
  const windows = await cachedExtractGsapWindows(script.content);
26586
+ const calls = [
26587
+ ...windows.map((win) => ({
26588
+ method: win.method,
26589
+ selector: win.targetSelector,
26590
+ properties: win.properties,
26591
+ raw: win.raw
26592
+ })),
26593
+ ...extractStandaloneGsapTransformCalls(stripJsComments(script.content))
26594
+ ];
26416
26595
  const conflicts = /* @__PURE__ */ new Map();
26417
- for (const win of windows) {
26418
- if (win.method === "fromTo" || win.method === "from") continue;
26419
- const sel = win.targetSelector;
26420
- const cssKey = sel.startsWith("#") || sel.startsWith(".") ? sel : `#${sel}`;
26421
- const translateProps = win.properties.filter(
26422
- (p2) => ["x", "y", "xPercent", "yPercent"].includes(p2)
26596
+ for (const call of calls) {
26597
+ if (call.method === "fromTo" || call.method === "from") continue;
26598
+ const sel = call.selector;
26599
+ const translateProps = call.properties.filter(
26600
+ (p2) => CONFLICTING_TRANSLATE_PROPS.includes(p2)
26423
26601
  );
26424
- const scaleProps = win.properties.filter((p2) => p2 === "scale");
26425
- const cssFromTranslate = translateProps.length > 0 ? cssTranslateSelectors.get(cssKey) : void 0;
26426
- const cssFromScale = scaleProps.length > 0 ? cssScaleSelectors.get(cssKey) : void 0;
26602
+ const scaleProps = call.properties.filter((p2) => CONFLICTING_SCALE_PROPS.includes(p2));
26603
+ const cssFromTranslate = translateProps.length > 0 ? matchCssTransform(sel, cssTranslateSelectors) : void 0;
26604
+ const cssFromScale = scaleProps.length > 0 ? matchCssTransform(sel, cssScaleSelectors) : void 0;
26427
26605
  if (!cssFromTranslate && !cssFromScale) continue;
26428
26606
  const existing = conflicts.get(sel) ?? {
26429
26607
  cssTransform: [cssFromTranslate, cssFromScale].filter(Boolean).join(" "),
26430
26608
  props: /* @__PURE__ */ new Set(),
26431
- raw: win.raw
26609
+ raw: call.raw
26432
26610
  };
26433
26611
  for (const p2 of [...translateProps, ...scaleProps]) existing.props.add(p2);
26434
26612
  conflicts.set(sel, existing);
@@ -26925,6 +27103,35 @@ function isCompositionRootOrMount(rawTag) {
26925
27103
  readAttr(rawTag, "data-composition-id") || readAttr(rawTag, "data-composition-src")
26926
27104
  );
26927
27105
  }
27106
+ function extractCssSelectors(css) {
27107
+ const out = [];
27108
+ const noComments = css.replace(/\/\*[\s\S]*?\*\//g, "");
27109
+ const ruleHeader = /([^{}]+)\{/g;
27110
+ let m2;
27111
+ while ((m2 = ruleHeader.exec(noComments)) !== null) {
27112
+ const header = (m2[1] ?? "").trim();
27113
+ if (!header || header.startsWith("@")) continue;
27114
+ for (const sel of header.split(",")) {
27115
+ const s2 = sel.trim();
27116
+ if (s2) out.push(s2);
27117
+ }
27118
+ }
27119
+ return out;
27120
+ }
27121
+ function leftmostCompoundClasses(selector) {
27122
+ const leftmost = selector.trim().split(/[\s>+~]+/)[0] ?? "";
27123
+ return (leftmost.match(/\.([\w-]+)/g) ?? []).map((c3) => c3.slice(1));
27124
+ }
27125
+ function rootClassStyledSelectors(styles, rootClasses) {
27126
+ const offenders = [];
27127
+ for (const style of styles) {
27128
+ for (const selector of extractCssSelectors(style.content)) {
27129
+ const hitsRoot = leftmostCompoundClasses(selector).some((c3) => rootClasses.includes(c3));
27130
+ if (hitsRoot && !offenders.includes(selector)) offenders.push(selector);
27131
+ }
27132
+ }
27133
+ return offenders;
27134
+ }
26928
27135
  var MAX_COMPOSITION_LINES, MAX_TIMED_ELEMENTS_PER_TRACK, TRACK_DENSITY_EXEMPT_TAGS, compositionRules;
26929
27136
  var init_composition = __esm({
26930
27137
  "../core/src/lint/rules/composition.ts"() {
@@ -27377,6 +27584,39 @@ var init_composition = __esm({
27377
27584
  });
27378
27585
  }
27379
27586
  return findings;
27587
+ },
27588
+ // subcomposition_root_styled_by_class
27589
+ // A sub-composition's <style> is scoped at render time to
27590
+ // `[data-composition-id="<id>"] <selector>` so scenes inlined into one document
27591
+ // can't leak styles into each other. A rule whose LEFTMOST selector is the ROOT
27592
+ // element's own class (e.g. `.frame { ... }` on the same element that carries
27593
+ // data-composition-id) therefore becomes a DESCENDANT selector that can never
27594
+ // match the root — the whole scene renders unstyled (tiny text top-left, images
27595
+ // at natural size). lint/validate/inspect evaluate the file in isolation (no
27596
+ // scoping) and Studio previews each scene in its own iframe (no scoping), so the
27597
+ // break is invisible until the composited MP4 render. Style the root via `#root`
27598
+ // (the scoper special-cases the root id) and descendants via plain selectors,
27599
+ // like the registry blocks — the runtime already scopes each scene by id, so a
27600
+ // class namespace on the root is redundant.
27601
+ ({ rootTag, rootCompositionId, styles, options }) => {
27602
+ if (!options.isSubComposition) return [];
27603
+ if (isRegistrySourceFile(options.filePath)) return [];
27604
+ if (!rootTag || !rootCompositionId) return [];
27605
+ const rootClasses = (readAttr(rootTag.raw, "class") || "").split(/\s+/).filter(Boolean);
27606
+ if (rootClasses.length === 0) return [];
27607
+ const offenders = rootClassStyledSelectors(styles, rootClasses);
27608
+ if (offenders.length === 0) return [];
27609
+ const example = offenders.slice(0, 3).join(", ");
27610
+ return [
27611
+ {
27612
+ code: "subcomposition_root_styled_by_class",
27613
+ severity: "error",
27614
+ message: `Root element has class="${rootClasses.join(" ")}" and is styled by ${offenders.length} rule(s) keyed off that class (e.g. ${example}). At render, every sub-composition rule is scoped to [data-composition-id="${rootCompositionId}"] <selector>, so a selector whose leftmost part is the ROOT's own class becomes a descendant selector that cannot match the root \u2014 the scene renders unstyled (tiny text top-left, full-size images). lint/validate/inspect and Studio's per-frame iframe preview do not scope, so this passes every static check and looks correct in preview.`,
27615
+ selector: example,
27616
+ fixHint: `Give the root id="root" and style it with \`#root { ... }\` plus plain descendant selectors (\`.kicker\`, \`#hero\`) \u2014 the runtime already scopes each sub-composition by data-composition-id, so a class namespace on the root is redundant and breaks under scoping.`,
27617
+ snippet: truncateSnippet(rootTag.raw)
27618
+ }
27619
+ ];
27380
27620
  }
27381
27621
  ];
27382
27622
  }
@@ -33175,11 +33415,10 @@ async function pageScreenshotCapture(page, options) {
33175
33415
  format: isPng ? "png" : "jpeg",
33176
33416
  quality: isPng ? void 0 : options.quality ?? 80,
33177
33417
  fromSurface: true,
33178
- // The explicit clip rect constrains output to exact composition
33179
- // dimensions. The viewport-boundary pre-clip from captureBeyondViewport:
33180
- // false is redundant, and Chrome's compositor rounds it inward under
33181
- // multi-tab load — clipping the bottom/right edge of tall viewports.
33182
- captureBeyondViewport: true,
33418
+ // Use Chrome's faster viewport-bound screenshot path by default. Callers
33419
+ // opt into the beyond-viewport path only for known compositor edge cases,
33420
+ // such as native video surfaces in tall portrait renders.
33421
+ captureBeyondViewport: options.captureBeyondViewport ?? false,
33183
33422
  optimizeForSpeed: !isPng,
33184
33423
  clip
33185
33424
  });
@@ -33194,8 +33433,8 @@ async function captureScreenshotWithAlpha(page, width, height) {
33194
33433
  const result = await client.send("Page.captureScreenshot", {
33195
33434
  format: "png",
33196
33435
  fromSurface: true,
33436
+ // Preserve the #1094 tall-portrait edge-clipping guard on HDR alpha captures.
33197
33437
  captureBeyondViewport: true,
33198
- // see pageScreenshotCapture for rationale
33199
33438
  optimizeForSpeed: false,
33200
33439
  // `true` uses a zero-alpha-aware fast path that crushes real alpha values — observed empirically, CDP docs don't spell it out
33201
33440
  clip: { x: 0, y: 0, width, height, scale: 1 }
@@ -33224,8 +33463,8 @@ async function captureAlphaPng(page, width, height) {
33224
33463
  const result = await client.send("Page.captureScreenshot", {
33225
33464
  format: "png",
33226
33465
  fromSurface: true,
33466
+ // Preserve the #1094 tall-portrait edge-clipping guard on HDR alpha captures.
33227
33467
  captureBeyondViewport: true,
33228
- // see pageScreenshotCapture for rationale
33229
33468
  optimizeForSpeed: false,
33230
33469
  // must be false to preserve alpha
33231
33470
  clip: { x: 0, y: 0, width, height, scale: 1 }
@@ -34491,7 +34730,11 @@ function getCapturePerfSummary(session) {
34491
34730
  staticDedupSkipReason: session.staticDedupSkipReason
34492
34731
  };
34493
34732
  }
34494
- var BROWSER_CONSOLE_BUFFER_SIZE, CAPTURE_SESSION_CLOSE_TIMEOUT_MS, LOCKED_WARMUP_TICKS, realSleep, HF_READY_DIAGNOSTIC_EXPR;
34733
+ function isTransientBrowserError(error) {
34734
+ const message = error instanceof Error ? error.message : String(error);
34735
+ return TRANSIENT_BROWSER_ERROR_PATTERNS.some((pattern) => pattern.test(message));
34736
+ }
34737
+ var BROWSER_CONSOLE_BUFFER_SIZE, CAPTURE_SESSION_CLOSE_TIMEOUT_MS, LOCKED_WARMUP_TICKS, realSleep, HF_READY_DIAGNOSTIC_EXPR, TRANSIENT_BROWSER_ERROR_PATTERNS;
34495
34738
  var init_frameCapture = __esm({
34496
34739
  "../engine/src/services/frameCapture.ts"() {
34497
34740
  "use strict";
@@ -34522,6 +34765,17 @@ var init_frameCapture = __esm({
34522
34765
  declaredDuration: declaredDuration,
34523
34766
  };
34524
34767
  })()`;
34768
+ TRANSIENT_BROWSER_ERROR_PATTERNS = [
34769
+ /Navigating frame was detached/i,
34770
+ /Target closed/i,
34771
+ /Session closed/i,
34772
+ /browser has disconnected/i,
34773
+ /Page crashed/i,
34774
+ /Execution context was destroyed/i,
34775
+ /Cannot find context with specified id/i,
34776
+ /Failed to launch the browser process/i,
34777
+ /ECONNREFUSED/i
34778
+ ];
34525
34779
  }
34526
34780
  });
34527
34781
 
@@ -37049,21 +37303,6 @@ function defaultBuildScopeSelector(compId) {
37049
37303
  const escaped = compId.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
37050
37304
  return `[data-composition-id="${escaped}"]`;
37051
37305
  }
37052
- function emptyCompositionHtmlError(src) {
37053
- return new Error(
37054
- `Composition HTML is empty or could not be parsed: ${src}. Check that the file referenced by data-composition-src contains valid HTML.`
37055
- );
37056
- }
37057
- function assertNonEmptyCompositionHtml(html, src) {
37058
- if (!html.trim()) {
37059
- throw emptyCompositionHtmlError(src);
37060
- }
37061
- }
37062
- function assertParsedCompositionDocument(doc, src) {
37063
- if (!doc.documentElement) {
37064
- throw emptyCompositionHtmlError(src);
37065
- }
37066
- }
37067
37306
  function inlineSubCompositions(document2, hosts, options) {
37068
37307
  const {
37069
37308
  resolveHtml,
@@ -37089,15 +37328,15 @@ function inlineSubCompositions(document2, hosts, options) {
37089
37328
  const src = hostEl.getAttribute("data-composition-src");
37090
37329
  if (!src) continue;
37091
37330
  const compHtml = resolveHtml(src);
37092
- if (compHtml == null) {
37093
- if (onMissingComposition) {
37094
- onMissingComposition(src);
37095
- }
37331
+ if (compHtml == null || !compHtml.trim()) {
37332
+ onMissingComposition?.(src);
37096
37333
  continue;
37097
37334
  }
37098
- assertNonEmptyCompositionHtml(compHtml, src);
37099
37335
  const compDoc = parseHtml2(compHtml);
37100
- assertParsedCompositionDocument(compDoc, src);
37336
+ if (!compDoc.documentElement) {
37337
+ onMissingComposition?.(src);
37338
+ continue;
37339
+ }
37101
37340
  let compId;
37102
37341
  let runtimeCompId;
37103
37342
  if (hostIdentityMap) {
@@ -37110,9 +37349,15 @@ function inlineSubCompositions(document2, hosts, options) {
37110
37349
  }
37111
37350
  const contentRoot = compDoc.querySelector("template");
37112
37351
  const contentHtml = contentRoot ? contentRoot.innerHTML || "" : compDoc.body?.innerHTML || "";
37113
- assertNonEmptyCompositionHtml(contentHtml, src);
37352
+ if (!contentHtml.trim()) {
37353
+ onMissingComposition?.(src);
37354
+ continue;
37355
+ }
37114
37356
  const contentDoc = parseHtml2(contentHtml);
37115
- assertParsedCompositionDocument(contentDoc, src);
37357
+ if (!contentDoc.documentElement) {
37358
+ onMissingComposition?.(src);
37359
+ continue;
37360
+ }
37116
37361
  const innerRoot = compId ? queryByAttr(contentDoc, "data-composition-id", compId) : contentDoc.querySelector("[data-composition-id]");
37117
37362
  const inferredCompId = innerRoot?.getAttribute("data-composition-id")?.trim() || "";
37118
37363
  const authoredRootId = innerRoot?.getAttribute("id")?.trim() || null;
@@ -41789,6 +42034,7 @@ __export(src_exports2, {
41789
42034
  isHdrColorSpace: () => isHdrColorSpace,
41790
42035
  isHttpUrl: () => isHttpUrl,
41791
42036
  isLowMemorySystem: () => isLowMemorySystem,
42037
+ isTransientBrowserError: () => isTransientBrowserError,
41792
42038
  isVideoFrameFormat: () => isVideoFrameFormat,
41793
42039
  killTrackedProcesses: () => killTrackedProcesses,
41794
42040
  launchHdrBrowser: () => launchHdrBrowser,
@@ -45203,6 +45449,7 @@ __export(manager_exports, {
45203
45449
  ensureModel: () => ensureModel,
45204
45450
  ensureWhisper: () => ensureWhisper,
45205
45451
  findWhisper: () => findWhisper,
45452
+ getInstallInstructions: () => getInstallInstructions,
45206
45453
  hasFFmpeg: () => hasFFmpeg,
45207
45454
  isWhisperUnavailable: () => isWhisperUnavailable
45208
45455
  });
@@ -45309,6 +45556,12 @@ function getInstallInstructions() {
45309
45556
  if (platform4() === "darwin") {
45310
45557
  return "brew install whisper-cpp";
45311
45558
  }
45559
+ if (platform4() === "linux") {
45560
+ return "Build from source: https://github.com/ggml-org/whisper.cpp#building (requires cmake and a C compiler)";
45561
+ }
45562
+ if (platform4() === "win32") {
45563
+ return "Build with cmake: https://github.com/ggml-org/whisper.cpp#building";
45564
+ }
45312
45565
  return "See https://github.com/ggml-org/whisper.cpp#building";
45313
45566
  }
45314
45567
  function hasBrew() {
@@ -45910,7 +46163,8 @@ var init_npxCommand = __esm({
45910
46163
  // src/commands/skills.ts
45911
46164
  var skills_exports = {};
45912
46165
  __export(skills_exports, {
45913
- default: () => skills_default
46166
+ default: () => skills_default,
46167
+ installAllSkills: () => installAllSkills
45914
46168
  });
45915
46169
  import { execFileSync as execFileSync4, spawn as spawn7 } from "child_process";
45916
46170
  function hasNpx() {
@@ -45922,12 +46176,13 @@ function hasNpx() {
45922
46176
  return false;
45923
46177
  }
45924
46178
  }
45925
- function runSkillsAdd(repo) {
45926
- const npx = buildNpxCommand(["skills", "add", repo, "--all"]);
46179
+ function runSkillsAdd(repo, opts = {}) {
46180
+ const npx = buildNpxCommand(["skills", "add", repo, ...opts.extraArgs ?? ["--all"]]);
45927
46181
  return new Promise((resolve58, reject) => {
45928
46182
  const child = spawn7(npx.command, npx.args, {
45929
46183
  stdio: "inherit",
45930
46184
  timeout: 12e4,
46185
+ cwd: opts.cwd,
45931
46186
  // GH #316 — the upstream `skills` CLI shells out to `git clone`.
45932
46187
  // When Git's clone-hook protection is active (shipped on by
45933
46188
  // default in 2.45.1, reverted in 2.45.2, still present on many
@@ -45945,6 +46200,22 @@ function runSkillsAdd(repo) {
45945
46200
  child.on("error", reject);
45946
46201
  });
45947
46202
  }
46203
+ async function installAllSkills(opts = {}) {
46204
+ if (!hasNpx()) {
46205
+ R2.error(c.error("npx not found. Install Node.js and retry."));
46206
+ return;
46207
+ }
46208
+ for (const source of SOURCES) {
46209
+ console.log();
46210
+ console.log(c.bold(`Installing ${source.name} skills...`));
46211
+ console.log();
46212
+ try {
46213
+ await runSkillsAdd(source.repo, opts);
46214
+ } catch {
46215
+ console.log(c.dim(`${source.name} skills skipped`));
46216
+ }
46217
+ }
46218
+ }
45948
46219
  var SOURCES, skills_default;
45949
46220
  var init_skills = __esm({
45950
46221
  "src/commands/skills.ts"() {
@@ -45961,20 +46232,7 @@ var init_skills = __esm({
45961
46232
  },
45962
46233
  args: {},
45963
46234
  async run() {
45964
- if (!hasNpx()) {
45965
- R2.error(c.error("npx not found. Install Node.js and retry."));
45966
- return;
45967
- }
45968
- for (const source of SOURCES) {
45969
- console.log();
45970
- console.log(c.bold(`Installing ${source.name} skills...`));
45971
- console.log();
45972
- try {
45973
- await runSkillsAdd(source.repo);
45974
- } catch {
45975
- console.log(c.dim(`${source.name} skills skipped`));
45976
- }
45977
- }
46235
+ await installAllSkills();
45978
46236
  }
45979
46237
  });
45980
46238
  }
@@ -46171,7 +46429,16 @@ async function lintProject(project) {
46171
46429
  const allHtmlSources = [{ html: rootHtml }];
46172
46430
  const compositionsDir = resolve12(project.dir, "compositions");
46173
46431
  if (existsSync22(compositionsDir)) {
46174
- const files = readdirSync8(compositionsDir).filter((f3) => f3.endsWith(".html"));
46432
+ const collectHtmlFiles = (dir, rel) => {
46433
+ const out = [];
46434
+ for (const entry of readdirSync8(dir, { withFileTypes: true })) {
46435
+ const relPath = rel ? `${rel}/${entry.name}` : entry.name;
46436
+ if (entry.isDirectory()) out.push(...collectHtmlFiles(join21(dir, entry.name), relPath));
46437
+ else if (entry.isFile() && entry.name.endsWith(".html")) out.push(relPath);
46438
+ }
46439
+ return out;
46440
+ };
46441
+ const files = collectHtmlFiles(compositionsDir, "").sort();
46175
46442
  for (const file of files) {
46176
46443
  const filePath = join21(compositionsDir, file);
46177
46444
  const html = readFileSync15(filePath, "utf-8");
@@ -93728,7 +93995,16 @@ var init_fileServer2 = __esm({
93728
93995
  var root = document.querySelector('[data-composition-id]');
93729
93996
  if (!root) return 0;
93730
93997
  var d = Number(root.getAttribute('data-duration'));
93731
- return Number.isFinite(d) && d > 0 ? d : 0;
93998
+ if (Number.isFinite(d) && d > 0) return d;
93999
+ var comps = document.querySelectorAll('[data-composition-src]');
94000
+ var maxEnd = 0;
94001
+ for (var i = 0; i < comps.length; i++) {
94002
+ var start = Number(comps[i].getAttribute('data-start')) || 0;
94003
+ var dur = Number(comps[i].getAttribute('data-duration')) || 0;
94004
+ if (dur > 0) maxEnd = Math.max(maxEnd, start + dur);
94005
+ }
94006
+ if (maxEnd > 0) console.warn('[HF Bridge] No root data-duration; derived ' + maxEnd + 's from sub-compositions');
94007
+ return maxEnd;
93732
94008
  }
93733
94009
  function seekSameOriginChildFrames(frameWindow, nextTimeMs) {
93734
94010
  var frames;
@@ -95272,7 +95548,10 @@ function inlineSubCompositions2(html, subCompositions, projectDir) {
95272
95548
  },
95273
95549
  parseHtml: (htmlStr) => parseHTML(htmlStr).document,
95274
95550
  scriptErrorLabel: "[Compiler] Composition script failed",
95275
- compoundAuthoredRoot: true
95551
+ compoundAuthoredRoot: true,
95552
+ onMissingComposition: (srcPath) => {
95553
+ console.warn(`[Compiler] Composition file missing or empty: ${srcPath}`);
95554
+ }
95276
95555
  }
95277
95556
  );
95278
95557
  for (const hostEl of hosts) {
@@ -96258,33 +96537,71 @@ async function runProbeStage(input2) {
96258
96537
  quality: needsAlpha ? void 0 : 80,
96259
96538
  deviceScaleFactor
96260
96539
  };
96261
- log2.info("Browser launched, creating capture session...");
96262
- probeSession = await createCaptureSession(
96263
- fileServer.url,
96264
- join44(workDir, "probe"),
96265
- captureOpts,
96266
- null,
96267
- probeCfg
96268
- );
96269
- log2.info("Waiting for composition to initialize...");
96270
- const initStart = Date.now();
96271
- const heartbeat = setInterval(() => {
96272
- const elapsed = ((Date.now() - initStart) / 1e3).toFixed(1);
96273
- log2.info(`Still waiting for browser initialization... (${elapsed}s elapsed)`);
96274
- }, 3e4);
96275
- try {
96276
- await initializeSession(probeSession);
96277
- } finally {
96278
- clearInterval(heartbeat);
96540
+ const PROBE_MAX_ATTEMPTS = 2;
96541
+ for (let attempt = 1; attempt <= PROBE_MAX_ATTEMPTS; attempt++) {
96542
+ const attemptStart = Date.now();
96543
+ try {
96544
+ log2.info("Creating capture session...", { attempt, maxAttempts: PROBE_MAX_ATTEMPTS });
96545
+ probeSession = await createCaptureSession(
96546
+ fileServer.url,
96547
+ join44(workDir, "probe"),
96548
+ captureOpts,
96549
+ null,
96550
+ probeCfg
96551
+ );
96552
+ log2.info("Waiting for composition to initialize...", { attempt });
96553
+ const heartbeat = setInterval(() => {
96554
+ const elapsed = ((Date.now() - attemptStart) / 1e3).toFixed(1);
96555
+ log2.info(`Still waiting for browser initialization... (${elapsed}s elapsed)`);
96556
+ }, 3e4);
96557
+ try {
96558
+ await initializeSession(probeSession);
96559
+ } finally {
96560
+ clearInterval(heartbeat);
96561
+ }
96562
+ } catch (err) {
96563
+ const isTransient = isTransientBrowserError(err);
96564
+ const errMsg = err instanceof Error ? err.message : String(err);
96565
+ log2.warn("Browser probe attempt failed", {
96566
+ attempt,
96567
+ maxAttempts: PROBE_MAX_ATTEMPTS,
96568
+ isTransient,
96569
+ error: errMsg,
96570
+ elapsedMs: Date.now() - attemptStart
96571
+ });
96572
+ if (probeSession) {
96573
+ try {
96574
+ await closeCaptureSession(probeSession);
96575
+ } catch (closeErr) {
96576
+ log2.warn("Failed to close crashed probe session", {
96577
+ error: closeErr instanceof Error ? closeErr.message : String(closeErr)
96578
+ });
96579
+ }
96580
+ probeSession = null;
96581
+ }
96582
+ if (isTransient && attempt < PROBE_MAX_ATTEMPTS) {
96583
+ log2.info("Retrying with a fresh browser session...", {
96584
+ attempt: attempt + 1,
96585
+ maxAttempts: PROBE_MAX_ATTEMPTS
96586
+ });
96587
+ assertNotAborted();
96588
+ continue;
96589
+ }
96590
+ throw err;
96591
+ }
96592
+ log2.info("Composition ready", {
96593
+ attempt,
96594
+ initMs: Date.now() - attemptStart
96595
+ });
96596
+ break;
96279
96597
  }
96280
- log2.info("Composition ready", {
96281
- initMs: Date.now() - initStart
96282
- });
96283
96598
  assertNotAborted();
96284
- lastBrowserConsole = probeSession.browserConsoleBuffer;
96599
+ const session = probeSession;
96600
+ probeSession = session;
96601
+ lastBrowserConsole = session.browserConsoleBuffer;
96285
96602
  if (composition.duration <= 0) {
96286
96603
  log2.info("Discovering composition duration...");
96287
- const discoveredDuration = await getCompositionDuration(probeSession);
96604
+ const discoveredDuration = await getCompositionDuration(session);
96288
96605
  assertNotAborted();
96289
96606
  log2.info("Probed composition duration from browser", {
96290
96607
  discoveredDuration,
@@ -96298,7 +96615,7 @@ async function runProbeStage(input2) {
96298
96615
  }
96299
96616
  if (compiled.unresolvedCompositions.length > 0) {
96300
96617
  const resolutions = await resolveCompositionDurations(
96301
- probeSession.page,
96618
+ session.page,
96302
96619
  compiled.unresolvedCompositions
96303
96620
  );
96304
96621
  assertNotAborted();
@@ -96317,7 +96634,7 @@ async function runProbeStage(input2) {
96317
96634
  }
96318
96635
  }
96319
96636
  log2.info("Discovering media assets from browser DOM...");
96320
- const browserMedia = await discoverMediaFromBrowser(probeSession.page);
96637
+ const browserMedia = await discoverMediaFromBrowser(session.page);
96321
96638
  assertNotAborted();
96322
96639
  if (browserMedia.length > 0) {
96323
96640
  const existingVideoIds = new Set(composition.videos.map((v2) => v2.id));
@@ -96408,7 +96725,7 @@ async function runProbeStage(input2) {
96408
96725
  audioCount: composition.audios.length
96409
96726
  });
96410
96727
  const automation = await discoverAudioVolumeAutomationFromTimeline(
96411
- probeSession.page,
96728
+ session.page,
96412
96729
  composition.audios.map((audio) => audio.id),
96413
96730
  composition.duration,
96414
96731
  fpsToNumber(job.config.fps)
@@ -96431,7 +96748,7 @@ async function runProbeStage(input2) {
96431
96748
  videoCount: composition.videos.length
96432
96749
  });
96433
96750
  const visibilityWindows = await discoverVideoVisibilityFromTimeline(
96434
- probeSession.page,
96751
+ session.page,
96435
96752
  composition.duration
96436
96753
  );
96437
96754
  assertNotAborted();
@@ -99550,6 +99867,12 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
99550
99867
  job.startedAt = /* @__PURE__ */ new Date();
99551
99868
  assertNotAborted();
99552
99869
  assertConfiguredFfmpegBinariesExist();
99870
+ if (!existsSync45(workDir)) mkdirSync26(workDir, { recursive: true });
99871
+ if (job.config.debug) {
99872
+ const logPath = join55(workDir, "render.log");
99873
+ restoreLogger = installDebugLogger(logPath, log2);
99874
+ log2.info("[Render] Debug artifacts enabled", { workDir, logPath });
99875
+ }
99553
99876
  log2.info("[Render] Pipeline started", {
99554
99877
  platform: process.platform,
99555
99878
  arch: process.arch,
@@ -99574,11 +99897,6 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
99574
99897
  playerReadyTimeoutMs: cfg.playerReadyTimeout,
99575
99898
  requestedWorkers: job.config.workers ?? "auto"
99576
99899
  });
99577
- if (!existsSync45(workDir)) mkdirSync26(workDir, { recursive: true });
99578
- if (job.config.debug) {
99579
- const logPath = join55(workDir, "render.log");
99580
- restoreLogger = installDebugLogger(logPath, log2);
99581
- }
99582
99900
  const entryFile = job.config.entryFile || "index.html";
99583
99901
  let htmlPath = join55(projectDir, entryFile);
99584
99902
  if (!existsSync45(htmlPath)) {
@@ -99796,8 +100114,12 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
99796
100114
  format: needsAlpha ? "png" : "jpeg",
99797
100115
  quality: needsAlpha ? void 0 : job.config.quality === "draft" ? 80 : 95,
99798
100116
  variables: job.config.variables,
99799
- deviceScaleFactor
100117
+ deviceScaleFactor,
100118
+ captureBeyondViewport: composition.videos.length > 0
99800
100119
  };
100120
+ updateCaptureObservability({
100121
+ captureBeyondViewport: captureOptions.captureBeyondViewport ?? false
100122
+ });
99801
100123
  const buildCaptureOptions = () => ({
99802
100124
  ...captureOptions,
99803
100125
  videoMetadataHints,
@@ -99926,6 +100248,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
99926
100248
  observability.checkpoint("capture_strategy", "resolved", {
99927
100249
  workerCount,
99928
100250
  forceScreenshot: captureForceScreenshot,
100251
+ captureBeyondViewport: captureOptions.captureBeyondViewport ?? false,
99929
100252
  useStreamingEncode,
99930
100253
  useLayeredComposite,
99931
100254
  usePageSideCompositing: usePageSideCompositingForTransitions,
@@ -102036,6 +102359,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
102036
102359
  // declare `data-composition-variables` leave this undefined and the
102037
102360
  // engine skips the `evaluateOnNewDocument` injection.
102038
102361
  variables: encoder.variables,
102362
+ captureBeyondViewport: (planVideos?.videos.length ?? 0) > 0,
102039
102363
  // lock the BeginFrame warmup loop to a fixed iteration count so
102040
102364
  // `beginFrameTimeTicks` is host-independent. Only chunks ever set this.
102041
102365
  lockWarmupTicks: true
@@ -104527,11 +104851,22 @@ var init_init = __esm({
104527
104851
  for (const f3 of readdirSync23(destDir2).filter((f4) => !f4.startsWith("."))) {
104528
104852
  console.log(` ${c.accent(f3)}`);
104529
104853
  }
104854
+ if (!skipSkills) {
104855
+ const { installAllSkills: installAllSkills2 } = await Promise.resolve().then(() => (init_skills(), skills_exports));
104856
+ const args2 = process.env["CLAUDECODE"] ? ["--agent", "claude-code", "--yes"] : ["--yes"];
104857
+ await installAllSkills2({ cwd: destDir2, extraArgs: args2 });
104858
+ }
104530
104859
  console.log();
104531
104860
  console.log("Get started:");
104532
104861
  console.log();
104533
- console.log(` ${c.accent("1.")} Install AI coding skills (one-time):`);
104534
- console.log(` ${c.accent("npx skills add heygen-com/hyperframes")}`);
104862
+ if (skipSkills) {
104863
+ console.log(` ${c.accent("1.")} Install AI coding skills (one-time):`);
104864
+ console.log(` ${c.accent("npx skills add heygen-com/hyperframes --yes")}`);
104865
+ } else {
104866
+ console.log(
104867
+ ` ${c.accent("1.")} Restart your AI agent (new session) so it loads the skills.`
104868
+ );
104869
+ }
104535
104870
  console.log();
104536
104871
  console.log(` ${c.accent("2.")} Open this project with your AI coding agent:`);
104537
104872
  console.log(
@@ -104718,8 +105053,8 @@ ${c.dim("Use --example blank for offline use.")}`
104718
105053
  process.exit(0);
104719
105054
  }
104720
105055
  if (installSkills) {
104721
- const skillsCmd = await Promise.resolve().then(() => (init_skills(), skills_exports)).then((m2) => m2.default);
104722
- await runCommand(skillsCmd, { rawArgs: [] });
105056
+ const { installAllSkills: installAllSkills2 } = await Promise.resolve().then(() => (init_skills(), skills_exports));
105057
+ await installAllSkills2({ cwd: destDir });
104723
105058
  }
104724
105059
  }
104725
105060
  R2.info("Opening studio preview...");
@@ -106649,6 +106984,10 @@ function buildDockerRunArgs(input2) {
106649
106984
  `${projectDir}:/project:ro`,
106650
106985
  "-v",
106651
106986
  `${outputDir}:/output`,
106987
+ // Keep debug artifacts on the mounted host output path. The producer roots
106988
+ // `.debug` at dirname(PRODUCER_RENDERS_DIR), so `/output/renders` maps to
106989
+ // `/output/.debug/<job id>` instead of a disposable container path.
106990
+ ...options.debug ? ["-e", "PRODUCER_RENDERS_DIR=/output/renders"] : [],
106652
106991
  imageTag,
106653
106992
  "/project",
106654
106993
  "--output",
@@ -106666,6 +107005,7 @@ function buildDockerRunArgs(input2) {
106666
107005
  ...options.videoBitrate ? ["--video-bitrate", options.videoBitrate] : [],
106667
107006
  ...options.videoFrameFormat && options.videoFrameFormat !== "auto" ? ["--video-frame-format", options.videoFrameFormat] : [],
106668
107007
  ...options.quiet ? ["--quiet"] : [],
107008
+ ...options.debug ? ["--debug"] : [],
106669
107009
  ...options.gpu ? ["--gpu"] : [],
106670
107010
  ...options.browserGpu ? [] : ["--no-browser-gpu"],
106671
107011
  ...options.hdrMode === "force-hdr" ? ["--hdr"] : [],
@@ -107413,6 +107753,7 @@ async function renderDocker(projectDir, outputPath, options) {
107413
107753
  entryFile: options.entryFile,
107414
107754
  outputResolution: options.outputResolution,
107415
107755
  pageSideCompositing: options.pageSideCompositing,
107756
+ debug: options.debug,
107416
107757
  pageNavigationTimeoutMs: options.pageNavigationTimeoutMs
107417
107758
  }
107418
107759
  });
@@ -107480,7 +107821,7 @@ async function renderLocal(projectDir, outputPath, options) {
107480
107821
  const producer = await loadProducer();
107481
107822
  const startTime = Date.now();
107482
107823
  const logger = createRenderTelemetryLogger(
107483
- producer.createConsoleLogger?.("info") ?? createNoopProducerLogger()
107824
+ producer.createConsoleLogger?.(options.debug ? "debug" : "info") ?? createNoopProducerLogger()
107484
107825
  );
107485
107826
  const job = producer.createRenderJob({
107486
107827
  fps: options.fps,
@@ -107503,7 +107844,8 @@ async function renderLocal(projectDir, outputPath, options) {
107503
107844
  videoFrameFormat: options.videoFrameFormat,
107504
107845
  variables: options.variables,
107505
107846
  entryFile: options.entryFile,
107506
- outputResolution: options.outputResolution
107847
+ outputResolution: options.outputResolution,
107848
+ debug: options.debug
107507
107849
  });
107508
107850
  const onProgress = options.quiet ? void 0 : (progressJob, message) => {
107509
107851
  renderProgress(progressJob.progress, message);
@@ -107882,6 +108224,11 @@ var init_render2 = __esm({
107882
108224
  description: "Suppress verbose output",
107883
108225
  default: false
107884
108226
  },
108227
+ debug: {
108228
+ type: "boolean",
108229
+ description: "Write full render diagnostics and keep intermediate artifacts under the producer .debug directory.",
108230
+ default: false
108231
+ },
107885
108232
  strict: {
107886
108233
  type: "boolean",
107887
108234
  description: "Fail render on lint errors",
@@ -108109,6 +108456,7 @@ var init_render2 = __esm({
108109
108456
  const browserGpuArg = args["browser-gpu"];
108110
108457
  const browserGpuMode = resolveBrowserGpuForCli(useDocker, browserGpuArg);
108111
108458
  const quiet = args.quiet ?? false;
108459
+ const debug = args.debug ?? false;
108112
108460
  const batchJson = args.json ?? false;
108113
108461
  const effectiveQuiet = quiet || batchPath != null && batchJson;
108114
108462
  const strictAll = args["strict-all"] ?? false;
@@ -108280,6 +108628,7 @@ var init_render2 = __esm({
108280
108628
  pageNavigationTimeoutMs,
108281
108629
  protocolTimeout,
108282
108630
  playerReadyTimeout,
108631
+ debug,
108283
108632
  exitAfterComplete: false,
108284
108633
  throwOnError: true,
108285
108634
  skipFeedback: true
@@ -108323,6 +108672,7 @@ var init_render2 = __esm({
108323
108672
  videoBitrate,
108324
108673
  videoFrameFormat,
108325
108674
  quiet,
108675
+ debug,
108326
108676
  variables,
108327
108677
  entryFile,
108328
108678
  outputResolution,
@@ -108348,6 +108698,7 @@ var init_render2 = __esm({
108348
108698
  videoFrameFormat,
108349
108699
  quiet,
108350
108700
  browserPath,
108701
+ debug,
108351
108702
  variables,
108352
108703
  entryFile,
108353
108704
  outputResolution,
@@ -112167,6 +112518,18 @@ function checkEnvironment() {
112167
112518
  }
112168
112519
  return { ok: true, detail: parts.join(" \xB7 ") };
112169
112520
  }
112521
+ async function checkWhisper() {
112522
+ const { findWhisper: findWhisper2, getInstallInstructions: getInstallInstructions2 } = await Promise.resolve().then(() => (init_manager(), manager_exports));
112523
+ const result = findWhisper2();
112524
+ if (result) {
112525
+ return { ok: true, detail: result.executablePath };
112526
+ }
112527
+ return {
112528
+ ok: false,
112529
+ detail: "Not found (optional \u2014 needed for transcription)",
112530
+ hint: getInstallInstructions2()
112531
+ };
112532
+ }
112170
112533
  function redactHome(s2) {
112171
112534
  const home = process.env["HOME"] || process.env["USERPROFILE"];
112172
112535
  if (!home) return s2;
@@ -112221,6 +112584,7 @@ var init_doctor = __esm({
112221
112584
  checks.push({ name: "/dev/shm", run: checkShm });
112222
112585
  }
112223
112586
  checks.push({ name: "Environment", run: checkEnvironment });
112587
+ checks.push({ name: "whisper-cpp", run: checkWhisper });
112224
112588
  const outcomes = [];
112225
112589
  for (const check of checks) {
112226
112590
  const result = await check.run();
@@ -112541,6 +112905,58 @@ Run ${c.accent("hyperframes telemetry --help")} for usage.`
112541
112905
  }
112542
112906
  });
112543
112907
 
112908
+ // src/commands/events.ts
112909
+ var events_exports2 = {};
112910
+ __export(events_exports2, {
112911
+ default: () => events_default
112912
+ });
112913
+ var ALLOWED_EVENTS, ALLOWED_OUTCOMES, SKILL_SLUG, events_default;
112914
+ var init_events2 = __esm({
112915
+ "src/commands/events.ts"() {
112916
+ "use strict";
112917
+ init_dist();
112918
+ init_client();
112919
+ ALLOWED_EVENTS = ["skill_invoked", "skill_completed"];
112920
+ ALLOWED_OUTCOMES = ["success", "error", "abort"];
112921
+ SKILL_SLUG = /^[a-z0-9][a-z0-9-]{0,63}$/;
112922
+ events_default = defineCommand({
112923
+ meta: {
112924
+ name: "events",
112925
+ description: "Emit an anonymous skill-usage telemetry event (skills report their own invocation/outcome). Honors DO_NOT_TRACK / telemetry opt-out."
112926
+ },
112927
+ args: {
112928
+ skill: {
112929
+ type: "string",
112930
+ description: "Authoring skill slug, e.g. product-launch-video"
112931
+ },
112932
+ event: {
112933
+ type: "string",
112934
+ description: "Event name: skill_invoked | skill_completed (default: skill_invoked)",
112935
+ default: "skill_invoked"
112936
+ },
112937
+ outcome: {
112938
+ type: "string",
112939
+ description: "Optional outcome for completion events: success | error | abort"
112940
+ }
112941
+ },
112942
+ async run({ args }) {
112943
+ try {
112944
+ const skill = typeof args.skill === "string" ? args.skill.trim() : "";
112945
+ if (!SKILL_SLUG.test(skill)) return;
112946
+ const event = ALLOWED_EVENTS.includes(args.event) ? args.event : "skill_invoked";
112947
+ const props = { authoring_skill: skill };
112948
+ if (args.outcome && ALLOWED_OUTCOMES.includes(args.outcome)) {
112949
+ props["outcome"] = args.outcome;
112950
+ }
112951
+ trackEvent(event, props);
112952
+ await flush();
112953
+ } catch {
112954
+ }
112955
+ }
112956
+ });
112957
+ }
112958
+ });
112959
+
112544
112960
  // src/utils/compositionViewport.ts
112545
112961
  function parseViewportDimension(value) {
112546
112962
  if (!value) return null;
@@ -162861,6 +163277,7 @@ var commandLoaders = {
162861
163277
  skills: () => Promise.resolve().then(() => (init_skills(), skills_exports)).then((m2) => m2.default),
162862
163278
  feedback: () => Promise.resolve().then(() => (init_feedback2(), feedback_exports)).then((m2) => m2.default),
162863
163279
  telemetry: () => Promise.resolve().then(() => (init_telemetry(), telemetry_exports)).then((m2) => m2.default),
163280
+ events: () => Promise.resolve().then(() => (init_events2(), events_exports2)).then((m2) => m2.default),
162864
163281
  validate: () => Promise.resolve().then(() => (init_validate(), validate_exports)).then((m2) => m2.default),
162865
163282
  snapshot: () => Promise.resolve().then(() => (init_snapshot(), snapshot_exports)).then((m2) => m2.default),
162866
163283
  capture: () => Promise.resolve().then(() => (init_capture2(), capture_exports2)).then((m2) => m2.default),
@@ -162891,7 +163308,7 @@ var _flushSync;
162891
163308
  var _trackCliError;
162892
163309
  var _trackCommandResult;
162893
163310
  var _printUpdateNotice;
162894
- if (!isHelp && command !== "telemetry" && command !== "unknown") {
163311
+ if (!isHelp && command !== "telemetry" && command !== "events" && command !== "unknown") {
162895
163312
  Promise.resolve().then(() => (init_telemetry2(), telemetry_exports2)).then((mod) => {
162896
163313
  _flush = mod.flush;
162897
163314
  _flushSync = mod.flushSync;
@@ -162902,7 +163319,7 @@ if (!isHelp && command !== "telemetry" && command !== "unknown") {
162902
163319
  if (mod.shouldTrack()) mod.incrementCommandCount();
162903
163320
  });
162904
163321
  }
162905
- if (!isHelp && !hasJsonFlag && command !== "upgrade") {
163322
+ if (!isHelp && !hasJsonFlag && command !== "upgrade" && command !== "events") {
162906
163323
  Promise.resolve().then(() => (init_autoUpdate(), autoUpdate_exports)).then((mod) => mod.reportCompletedUpdate()).catch(() => {
162907
163324
  });
162908
163325
  Promise.resolve().then(() => (init_updateCheck(), updateCheck_exports)).then(async (mod) => {