hyperframes 0.7.8 → 0.7.10

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.8" : "0.0.0-dev";
53
+ VERSION = true ? "0.7.10" : "0.0.0-dev";
54
54
  }
55
55
  });
56
56
 
@@ -21074,6 +21074,10 @@ function resolveTimelinePositions(anims) {
21074
21074
  let cursor = 0;
21075
21075
  let prevStart = 0;
21076
21076
  for (const anim of anims) {
21077
+ if (anim.method === "set" && anim.global) {
21078
+ anim.resolvedStart = 0;
21079
+ continue;
21080
+ }
21077
21081
  const duration = anim.method === "set" ? 0 : anim.duration ?? GSAP_DEFAULT_DURATION;
21078
21082
  let start;
21079
21083
  if (anim.implicitPosition) {
@@ -25380,11 +25384,40 @@ function describeStudioElement(tag) {
25380
25384
  parts.push(">");
25381
25385
  return parts.join("");
25382
25386
  }
25383
- var coreRules;
25387
+ function findCodeFenceLeak(headWithoutValidBlocks) {
25388
+ return MARKDOWN_CODE_FENCE_PATTERN.exec(headWithoutValidBlocks)?.[0] ?? null;
25389
+ }
25390
+ function findOrphanCssLeak(headContent) {
25391
+ const residualText = headContent.replace(HEAD_BLOCKS_TO_IGNORE_PATTERN, " ").replace(HTML_TAG_PATTERN, " ");
25392
+ return ORPHAN_CSS_AT_RULE_PATTERN.exec(residualText)?.[0] ?? ORPHAN_CSS_RULE_PATTERN.exec(residualText)?.[0] ?? null;
25393
+ }
25394
+ function findStrayCloseLeak(headWithoutValidBlocks) {
25395
+ return STRAY_HEAD_CLOSE_PATTERN.exec(headWithoutValidBlocks)?.[0] ?? null;
25396
+ }
25397
+ function findLeakedTextInHeadContent(headContent) {
25398
+ const withoutValidBlocks = headContent.replace(HEAD_BLOCKS_TO_IGNORE_PATTERN, " ");
25399
+ return findCodeFenceLeak(withoutValidBlocks) ?? findOrphanCssLeak(headContent) ?? findStrayCloseLeak(withoutValidBlocks);
25400
+ }
25401
+ function findLeakedTextInHead(rawSource) {
25402
+ const headMatches = [...rawSource.matchAll(HEAD_CONTENT_PATTERN)];
25403
+ for (const match of headMatches) {
25404
+ const leakedText = findLeakedTextInHeadContent(match[1] ?? "");
25405
+ if (leakedText) return leakedText;
25406
+ }
25407
+ return null;
25408
+ }
25409
+ var HEAD_BLOCKS_TO_IGNORE_PATTERN, HTML_TAG_PATTERN, HEAD_CONTENT_PATTERN, STRAY_HEAD_CLOSE_PATTERN, MARKDOWN_CODE_FENCE_PATTERN, ORPHAN_CSS_AT_RULE_PATTERN, ORPHAN_CSS_RULE_PATTERN, coreRules;
25384
25410
  var init_core = __esm({
25385
25411
  "../core/src/lint/rules/core.ts"() {
25386
25412
  "use strict";
25387
25413
  init_utils2();
25414
+ HEAD_BLOCKS_TO_IGNORE_PATTERN = /<(?:style|script|template|title|noscript)\b[^>]*>[\s\S]*?<\/(?:style|script|template|title|noscript)(?:\s[^>]*)?>/gi;
25415
+ HTML_TAG_PATTERN = /<[^>]+>/g;
25416
+ HEAD_CONTENT_PATTERN = /<head\b[^>]*>([\s\S]*?)(?:<\/head>|<body\b|$)/gi;
25417
+ STRAY_HEAD_CLOSE_PATTERN = /<\/(?:style|script)(?:\s[^>]*)?>/i;
25418
+ MARKDOWN_CODE_FENCE_PATTERN = /```[^\r\n`]*(?:\r?\n|$)[\s\S]*?```/i;
25419
+ ORPHAN_CSS_AT_RULE_PATTERN = /(?:^|\s)@(?:container|font-face|keyframes|layer|media|page|property|scope|supports)[^{<]*\{[\s\S]*?:[\s\S]*?\}/i;
25420
+ ORPHAN_CSS_RULE_PATTERN = /(?:^|\s)(?:\/\*[\s\S]*?\*\/\s*)?(?:@[a-z-]+[^{}<]*|[.#][\w-]+[^{}<]*|[a-z][\w-]*(?:\s+[.#:[\w-][^{}<]*)?)\s*\{[^{}]*:[^{}]*\}/i;
25388
25421
  coreRules = [
25389
25422
  // root_missing_composition_id + root_missing_dimensions
25390
25423
  ({ rootTag }) => {
@@ -25411,6 +25444,20 @@ var init_core = __esm({
25411
25444
  }
25412
25445
  return findings;
25413
25446
  },
25447
+ // head_leaked_text
25448
+ ({ source }) => {
25449
+ const snippet = findLeakedTextInHead(source);
25450
+ if (!snippet) return [];
25451
+ return [
25452
+ {
25453
+ code: "head_leaked_text",
25454
+ severity: "error",
25455
+ message: "Detected leaked code or CSS text in the document `<head>`. Browsers render this as visible text in the video.",
25456
+ fixHint: "Move CSS into a single `<style>...</style>` block and remove stray close tags, markdown fences, or code text from `<head>`.",
25457
+ snippet: truncateSnippet(snippet)
25458
+ }
25459
+ ];
25460
+ },
25414
25461
  // missing_timeline_registry + timeline_registry_missing_init
25415
25462
  ({ source, rawSource, options }) => {
25416
25463
  if (options.isSubComposition || rawSource.trimStart().toLowerCase().startsWith("<template")) {
@@ -32424,6 +32471,7 @@ __export(src_exports, {
32424
32471
  HYPERFRAME_RUNTIME_GLOBALS: () => HYPERFRAME_RUNTIME_GLOBALS,
32425
32472
  ITEM_TYPES: () => ITEM_TYPES,
32426
32473
  ITEM_TYPE_DIRS: () => ITEM_TYPE_DIRS,
32474
+ MEDIA_DURATION_CLAMP_EPSILON_SECONDS: () => MEDIA_DURATION_CLAMP_EPSILON_SECONDS,
32427
32475
  MEDIA_STYLES: () => MEDIA_STYLES,
32428
32476
  MEDIA_VISUAL_STYLE_PROPERTIES: () => MEDIA_VISUAL_STYLE_PROPERTIES,
32429
32477
  TEXT_STYLES: () => TEXT_STYLES,
@@ -39113,6 +39161,15 @@ function getFrameAtTime(extracted, globalTime, videoStart, loop = false, mediaSt
39113
39161
  if (frameIndex < 0 || frameIndex >= extracted.totalFrames) return null;
39114
39162
  return extracted.framePaths.get(frameIndex) || null;
39115
39163
  }
39164
+ function analyzeClipMediaFit(params) {
39165
+ const { slotSeconds, mediaSeconds, loop } = params;
39166
+ if (loop) return null;
39167
+ if (!(slotSeconds > 0) || !Number.isFinite(mediaSeconds) || mediaSeconds < 0) return null;
39168
+ const toleranceSeconds = MEDIA_DURATION_CLAMP_EPSILON_SECONDS;
39169
+ const shortfallSeconds = slotSeconds - mediaSeconds;
39170
+ if (shortfallSeconds <= toleranceSeconds) return null;
39171
+ return { shortfallSeconds, toleranceSeconds };
39172
+ }
39116
39173
  function createFrameLookupTable(videos, extracted) {
39117
39174
  const table = new FrameLookupTable();
39118
39175
  const extractedMap = /* @__PURE__ */ new Map();
@@ -39123,7 +39180,7 @@ function createFrameLookupTable(videos, extracted) {
39123
39180
  }
39124
39181
  return table;
39125
39182
  }
39126
- var VIDEO_FRAME_FORMATS, ALPHA_CAPABLE_CODECS, FrameLookupTable;
39183
+ var VIDEO_FRAME_FORMATS, ALPHA_CAPABLE_CODECS, HOLD_LAST_FRAME_TOLERANCE_FRAMES, FrameLookupTable;
39127
39184
  var init_videoFrameExtractor = __esm({
39128
39185
  "../engine/src/services/videoFrameExtractor.ts"() {
39129
39186
  "use strict";
@@ -39140,6 +39197,7 @@ var init_videoFrameExtractor = __esm({
39140
39197
  init_extractionCache();
39141
39198
  VIDEO_FRAME_FORMATS = ["auto", "jpg", "png"];
39142
39199
  ALPHA_CAPABLE_CODECS = /* @__PURE__ */ new Set(["vp9", "vp8", "prores"]);
39200
+ HOLD_LAST_FRAME_TOLERANCE_FRAMES = 2;
39143
39201
  FrameLookupTable = class {
39144
39202
  videos = /* @__PURE__ */ new Map();
39145
39203
  orderedVideos = [];
@@ -39218,7 +39276,12 @@ var init_videoFrameExtractor = __esm({
39218
39276
  continue;
39219
39277
  }
39220
39278
  if (frameIndex < 0 || frameIndex >= video.extracted.totalFrames) {
39221
- if (globalTime >= video.end && video.extracted.totalFrames > 0) {
39279
+ const fps = video.extracted.fps;
39280
+ const holdTolerance = Math.max(
39281
+ fps > 0 ? HOLD_LAST_FRAME_TOLERANCE_FRAMES / fps : 0,
39282
+ MEDIA_DURATION_CLAMP_EPSILON_SECONDS
39283
+ );
39284
+ if (globalTime >= video.end - holdTolerance && video.extracted.totalFrames > 0) {
39222
39285
  const lastIndex = video.extracted.totalFrames - 1;
39223
39286
  const lastPath = video.extracted.framePaths.get(lastIndex);
39224
39287
  if (lastPath) frames.set(videoId, { framePath: lastPath, frameIndex: lastIndex });
@@ -42099,6 +42162,7 @@ __export(src_exports2, {
42099
42162
  TRANSITIONS: () => TRANSITIONS,
42100
42163
  VIDEO_FRAME_FORMATS: () => VIDEO_FRAME_FORMATS,
42101
42164
  acquireBrowser: () => acquireBrowser,
42165
+ analyzeClipMediaFit: () => analyzeClipMediaFit,
42102
42166
  analyzeCompositionHdr: () => analyzeCompositionHdr,
42103
42167
  analyzeKeyframeIntervals: () => analyzeKeyframeIntervals,
42104
42168
  applyDomLayerMask: () => applyDomLayerMask,
@@ -84503,6 +84567,10 @@ function resolveTimelinePositions2(anims) {
84503
84567
  let cursor = 0;
84504
84568
  let prevStart = 0;
84505
84569
  for (const anim of anims) {
84570
+ if (anim.method === "set" && anim.global) {
84571
+ anim.resolvedStart = 0;
84572
+ continue;
84573
+ }
84506
84574
  const duration = anim.method === "set" ? 0 : anim.duration ?? GSAP_DEFAULT_DURATION2;
84507
84575
  let start;
84508
84576
  if (anim.implicitPosition) {
@@ -95590,7 +95658,7 @@ async function resolveMediaDuration(src, mediaStart, baseDir, downloadDir, tagNa
95590
95658
  const duration = effectiveDuration > 0 ? effectiveDuration : fileDuration;
95591
95659
  return { duration, resolvedPath: filePath };
95592
95660
  }
95593
- async function compileHtmlFile(html, baseDir, downloadDir) {
95661
+ async function compileHtmlFile(html, baseDir, downloadDir, log2) {
95594
95662
  const { html: staticCompiled, unresolved } = compileTimingAttrs(html);
95595
95663
  const mediaUnresolved = unresolved.filter(
95596
95664
  (el) => (el.tagName === "video" || el.tagName === "audio") && el.src
@@ -95615,13 +95683,17 @@ async function compileHtmlFile(html, baseDir, downloadDir) {
95615
95683
  downloadDir,
95616
95684
  el.tagName
95617
95685
  );
95618
- return { id: el.id, duration: el.duration, maxDuration, src: el.src };
95686
+ return { id: el.id, tagName: el.tagName, duration: el.duration, maxDuration, src: el.src };
95619
95687
  })
95620
95688
  );
95621
95689
  const clampList = [];
95622
95690
  for (const r2 of clampResults) {
95623
95691
  if (r2.maxDuration > 0 && shouldClampMediaDuration(r2.duration, r2.maxDuration)) {
95624
95692
  clampList.push({ id: r2.id, duration: r2.maxDuration });
95693
+ const kind = r2.tagName === "audio" ? "Audio" : "Video";
95694
+ log2?.warn(
95695
+ `[compile] ${kind} "${r2.id}" (${r2.src}) is ${r2.maxDuration.toFixed(2)}s but its data-duration is ${r2.duration.toFixed(2)}s \u2014 the slot is shortened to the media length. Set data-duration to ~${r2.maxDuration.toFixed(2)}s, trim data-media-start, or use a longer/looping source if that isn't intended.`
95696
+ );
95625
95697
  }
95626
95698
  }
95627
95699
  if (clampList.length > 0) {
@@ -96310,7 +96382,8 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
96310
96382
  const { html: compiledHtml, unresolvedCompositions } = await compileHtmlFile(
96311
96383
  rawHtml,
96312
96384
  projectDir,
96313
- downloadDir
96385
+ downloadDir,
96386
+ options.log
96314
96387
  );
96315
96388
  const {
96316
96389
  videos: subVideos,
@@ -96709,6 +96782,7 @@ async function runCompileStage(input2) {
96709
96782
  } = input2;
96710
96783
  const compileStart = Date.now();
96711
96784
  const compiled = await compileForRender(projectDir, htmlPath, join43(workDir, "downloads"), {
96785
+ log: log2,
96712
96786
  failClosedFontFetch: failClosedFontFetch === true,
96713
96787
  allowSystemFontCapture,
96714
96788
  animatedGifCacheDir: cfg.extractCacheDir ? join43(cfg.extractCacheDir, "animated-gif") : void 0,
@@ -101912,7 +101986,18 @@ function validateNoSystemFonts(compiledHtml) {
101912
101986
  );
101913
101987
  }
101914
101988
  }
101915
- var PlanValidationError, SYSTEM_FONT_USED;
101989
+ function validateDistributedDuration(input2) {
101990
+ const { duration, totalFrames, fps } = input2;
101991
+ const maxFrames = Math.ceil(MAX_DISTRIBUTED_DURATION_SECONDS * fps);
101992
+ if (Number.isFinite(duration) && duration > 0 && Number.isFinite(fps) && fps > 0 && Number.isSafeInteger(totalFrames) && totalFrames > 0 && totalFrames <= maxFrames) {
101993
+ return;
101994
+ }
101995
+ throw new PlanValidationError(
101996
+ DISTRIBUTED_DURATION_OUT_OF_RANGE,
101997
+ `[planValidation] Distributed render duration is out of range: duration=${String(duration)}s totalFrames=${String(totalFrames)} fps=${String(fps)} (maxDuration=${String(MAX_DISTRIBUTED_DURATION_SECONDS)}s, maxFrames=${String(maxFrames)}). This usually means an unbounded timeline escaped into render planning, such as GSAP repeat:-1 / yoyo loops without an explicit finite root duration. Add a finite data-duration or replace infinite repeats with a finite repeat count before rendering.`
101998
+ );
101999
+ }
102000
+ var PlanValidationError, SYSTEM_FONT_USED, DISTRIBUTED_DURATION_OUT_OF_RANGE, MAX_DISTRIBUTED_DURATION_SECONDS;
101916
102001
  var init_planValidation = __esm({
101917
102002
  "../producer/src/services/render/planValidation.ts"() {
101918
102003
  "use strict";
@@ -101929,6 +102014,8 @@ var init_planValidation = __esm({
101929
102014
  }
101930
102015
  };
101931
102016
  SYSTEM_FONT_USED = "SYSTEM_FONT_USED";
102017
+ DISTRIBUTED_DURATION_OUT_OF_RANGE = "DISTRIBUTED_DURATION_OUT_OF_RANGE";
102018
+ MAX_DISTRIBUTED_DURATION_SECONDS = 24 * 60 * 60;
101932
102019
  }
101933
102020
  });
101934
102021
 
@@ -102314,6 +102401,11 @@ async function plan(projectDir, config, planDir) {
102314
102401
  job.duration = probeResult.duration;
102315
102402
  job.totalFrames = probeResult.totalFrames;
102316
102403
  const totalFrames = probeResult.totalFrames;
102404
+ validateDistributedDuration({
102405
+ duration: probeResult.duration,
102406
+ totalFrames,
102407
+ fps: fpsToNumber(job.config.fps)
102408
+ });
102317
102409
  if (probeResult.fileServer) closeFileServerSafely(probeResult.fileServer, "plan", log2);
102318
102410
  if (probeResult.probeSession) {
102319
102411
  try {
@@ -102456,6 +102548,7 @@ var PLAN_PROJECT_DIR_SKIP_SEGMENTS, DEFAULT_MAX_PARALLEL_CHUNKS, MIN_CHUNK_SIZE,
102456
102548
  var init_plan = __esm({
102457
102549
  "../producer/src/services/distributed/plan.ts"() {
102458
102550
  "use strict";
102551
+ init_src();
102459
102552
  init_src2();
102460
102553
  init_logger();
102461
102554
  init_fileServer2();
@@ -102531,7 +102624,7 @@ function rebuildExtractedFramesFromPlanDir(planDir, videos) {
102531
102624
  for (let i2 = 0; i2 < frames.length; i2++) {
102532
102625
  const frameName = frames[i2];
102533
102626
  if (!frameName) continue;
102534
- framePaths.set(i2 + 1, join61(outputDir, frameName));
102627
+ framePaths.set(i2, join61(outputDir, frameName));
102535
102628
  }
102536
102629
  result.push({
102537
102630
  videoId: v2.videoId,
@@ -103471,6 +103564,7 @@ var init_distributed = __esm({
103471
103564
  init_assemble();
103472
103565
  init_renderConfigValidation();
103473
103566
  init_projectHash();
103567
+ init_planValidation();
103474
103568
  }
103475
103569
  });
103476
103570
 
@@ -113556,6 +113650,47 @@ async function seekTo2(page, time) {
113556
113650
  }, time);
113557
113651
  await new Promise((r2) => setTimeout(r2, SEEK_SETTLE_MS2));
113558
113652
  }
113653
+ async function auditClipDurations(page, analyzeClipMediaFit2) {
113654
+ const clips = await page.evaluate(() => {
113655
+ const rows = [];
113656
+ document.querySelectorAll("video[data-duration], audio[data-duration]").forEach((node) => {
113657
+ const el = node;
113658
+ const slot = parseFloat(el.getAttribute("data-duration") ?? "");
113659
+ if (!(slot > 0)) return;
113660
+ rows.push({
113661
+ id: el.id || el.getAttribute("src") || `(${el.tagName.toLowerCase()})`,
113662
+ kind: el.tagName === "AUDIO" ? "Audio" : "Video",
113663
+ slot,
113664
+ mediaStart: parseFloat(el.getAttribute("data-media-start") ?? "0") || 0,
113665
+ duration: el.duration,
113666
+ loop: el.loop || el.getAttribute("data-loop") === "true"
113667
+ });
113668
+ });
113669
+ return rows;
113670
+ });
113671
+ const warnings = [];
113672
+ const unreadable = [];
113673
+ for (const clip of clips) {
113674
+ if (!Number.isFinite(clip.duration) || clip.duration <= 0) {
113675
+ unreadable.push(clip.id);
113676
+ continue;
113677
+ }
113678
+ const mediaSeconds = Math.max(0, clip.duration - clip.mediaStart);
113679
+ const fit = analyzeClipMediaFit2({ slotSeconds: clip.slot, mediaSeconds, loop: clip.loop });
113680
+ if (!fit) continue;
113681
+ warnings.push({
113682
+ level: "warning",
113683
+ text: `${clip.kind} "${clip.id}" is ${mediaSeconds.toFixed(2)}s but its slot (data-duration) is ${clip.slot.toFixed(2)}s \u2014 the slot is shortened to the media length when rendered. Set data-duration to ~${mediaSeconds.toFixed(2)}s if that isn't intended.`
113684
+ });
113685
+ }
113686
+ if (unreadable.length > 0) {
113687
+ warnings.push({
113688
+ level: "warning",
113689
+ text: `Could not read the duration of ${unreadable.length} media element(s) within the validate timeout (${unreadable.join(", ")}); their slot vs. source fit was not checked. Re-run with a longer --timeout if the source is slow to load.`
113690
+ });
113691
+ }
113692
+ return warnings;
113693
+ }
113559
113694
  async function runContrastAudit(page) {
113560
113695
  const duration = await getCompositionDuration3(page);
113561
113696
  if (duration <= 0) return [];
@@ -113597,7 +113732,7 @@ async function validateInBrowser(projectDir, opts) {
113597
113732
  try {
113598
113733
  const browser = await ensureBrowser2();
113599
113734
  const puppeteer = await import("puppeteer-core");
113600
- const { buildChromeArgs: buildChromeArgs2 } = await Promise.resolve().then(() => (init_src2(), src_exports2));
113735
+ const { buildChromeArgs: buildChromeArgs2, analyzeClipMediaFit: analyzeClipMediaFit2 } = await Promise.resolve().then(() => (init_src2(), src_exports2));
113601
113736
  const browserGpuMode = process.env.PRODUCER_BROWSER_GPU_MODE === "software" ? "software" : "hardware";
113602
113737
  const chromeBrowser = await puppeteer.default.launch({
113603
113738
  headless: true,
@@ -113644,6 +113779,9 @@ async function validateInBrowser(projectDir, opts) {
113644
113779
  });
113645
113780
  await page.goto(server.url, { waitUntil: "domcontentloaded", timeout: 1e4 });
113646
113781
  await new Promise((r2) => setTimeout(r2, opts.timeout ?? 3e3));
113782
+ for (const w3 of await auditClipDurations(page, analyzeClipMediaFit2)) {
113783
+ warnings.push(w3);
113784
+ }
113647
113785
  if (opts.contrast) {
113648
113786
  contrast = await runContrastAudit(page);
113649
113787
  }