hyperframes 0.8.34 → 0.8.35

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.8.34" : "0.0.0-dev";
53
+ VERSION = true ? "0.8.35" : "0.0.0-dev";
54
54
  }
55
55
  });
56
56
 
@@ -65353,6 +65353,34 @@ var init_pageNavigationTimeoutErrorHint = __esm({
65353
65353
  }
65354
65354
  });
65355
65355
 
65356
+ // ../engine/src/services/drawElementCaptureError.ts
65357
+ function isDrawElementCaptureError(error) {
65358
+ let current2 = error;
65359
+ for (let depth = 0; depth < 5 && typeof current2 === "object" && current2 !== null; depth++) {
65360
+ if ("deCaptureFailure" in current2 && current2.deCaptureFailure === true) return true;
65361
+ current2 = "cause" in current2 ? current2.cause : void 0;
65362
+ }
65363
+ return false;
65364
+ }
65365
+ var DrawElementCaptureError;
65366
+ var init_drawElementCaptureError = __esm({
65367
+ "../engine/src/services/drawElementCaptureError.ts"() {
65368
+ "use strict";
65369
+ DrawElementCaptureError = class extends Error {
65370
+ frameIndex;
65371
+ constructor(frameIndex, reason, cause) {
65372
+ super(
65373
+ `drawElement capture failed at frame ${frameIndex}: ${reason}; fresh screenshot capture required`,
65374
+ { cause }
65375
+ );
65376
+ this.name = "DrawElementCaptureError";
65377
+ Object.defineProperty(this, "deCaptureFailure", { value: true, enumerable: true });
65378
+ this.frameIndex = frameIndex;
65379
+ }
65380
+ };
65381
+ }
65382
+ });
65383
+
65356
65384
  // ../engine/src/services/screenshotService.ts
65357
65385
  async function getCdpSession(page) {
65358
65386
  let client = cdpSessionCache.get(page);
@@ -69417,7 +69445,10 @@ async function captureFrameCore(session, frameIndex, time) {
69417
69445
  else session.beginFrameNoDamageCount++;
69418
69446
  screenshotBuffer = result.buffer;
69419
69447
  } else if (session.captureMode === "drawelement" && session.clipBoundaryFrames?.has(frameIndex) && process.env.HF_FAST_CAPTURE_BOUNDARY_SS === "true") {
69420
- screenshotBuffer = await pageScreenshotCapture(page, options);
69448
+ throw new DrawElementCaptureError(
69449
+ frameIndex,
69450
+ "boundary screenshot requested on an injected canvas page"
69451
+ );
69421
69452
  } else if (session.captureMode === "drawelement") {
69422
69453
  if (session.beginFrameTimeTicks > 0) {
69423
69454
  const client = await getCdpSession(page);
@@ -69446,10 +69477,10 @@ async function captureFrameCore(session, frameIndex, time) {
69446
69477
  const median3 = sorted ? sorted[sorted.length >> 1] ?? 0 : 0;
69447
69478
  const floor = Math.max(2e4, median3 * 0.12);
69448
69479
  if (screenshotBuffer.length < floor) {
69449
- console.log(
69450
- `[engine] fast capture: frame ${frameIndex} \u2014 drawElement frame anomalously small (${screenshotBuffer.length}B < ${Math.round(floor)}B, likely a silent paint-record drop); screenshot fallback (see fast-capture-limitations.md)`
69480
+ throw new DrawElementCaptureError(
69481
+ frameIndex,
69482
+ `suspect small frame (${screenshotBuffer.length}B < ${Math.round(floor)}B)`
69451
69483
  );
69452
- screenshotBuffer = await pageScreenshotCapture(page, options);
69453
69484
  } else {
69454
69485
  if (sizes.length >= 60) sizes.shift();
69455
69486
  sizes.push(screenshotBuffer.length);
@@ -69459,10 +69490,7 @@ async function captureFrameCore(session, frameIndex, time) {
69459
69490
  if (isRecoverableDrawElementError(err)) {
69460
69491
  session.deNcprFallbacks = (session.deNcprFallbacks ?? 0) + 1;
69461
69492
  const reason = isCanvasNotInitializedError(err) ? "drawElement canvas not initialized" : "No cached paint record";
69462
- console.log(
69463
- `[engine] fast capture: frame ${frameIndex} \u2014 ${reason}; screenshot fallback for this frame (see fast-capture-limitations.md)`
69464
- );
69465
- screenshotBuffer = await pageScreenshotCapture(page, options);
69493
+ throw new DrawElementCaptureError(frameIndex, reason, err);
69466
69494
  } else {
69467
69495
  throw err;
69468
69496
  }
@@ -69551,21 +69579,10 @@ async function captureFrameToBufferPipelined(session, frameIndex, time) {
69551
69579
  );
69552
69580
  void quantizedTime;
69553
69581
  if (session.clipBoundaryFrames?.has(frameIndex) && process.env.HF_FAST_CAPTURE_BOUNDARY_SS === "true") {
69554
- const buffer = await pageScreenshotCapture(page, options);
69555
- session.capturePerf.frames += 1;
69556
- session.capturePerf.seekMs += seekMs;
69557
- session.capturePerf.beforeCaptureMs += beforeCaptureMs;
69558
- {
69559
- const boundaryMs = Date.now() - startTime;
69560
- session.capturePerf.totalMs += boundaryMs;
69561
- session.capturePerf.frameMs.push(boundaryMs);
69562
- }
69563
- const boundaryResult = Promise.resolve(buffer);
69564
- if (session.staticFrames) {
69565
- session.lastEncodeResult = boundaryResult;
69566
- session.lastEncodeResultFrame = frameIndex;
69567
- }
69568
- return { encodeResult: boundaryResult, captureTimeMs: Date.now() - startTime };
69582
+ throw new DrawElementCaptureError(
69583
+ frameIndex,
69584
+ "boundary screenshot requested on an injected canvas page"
69585
+ );
69569
69586
  }
69570
69587
  const { encodeResult } = await produceDrawElementFrame(
69571
69588
  page,
@@ -69587,15 +69604,6 @@ async function captureFrameToBufferPipelined(session, frameIndex, time) {
69587
69604
  }
69588
69605
  return { encodeResult, captureTimeMs };
69589
69606
  } catch (captureError) {
69590
- if (isRecoverableDrawElementError(captureError)) {
69591
- session.deNcprFallbacks = (session.deNcprFallbacks ?? 0) + 1;
69592
- const reason = isCanvasNotInitializedError(captureError) ? "drawElement canvas not initialized" : "No cached paint record";
69593
- console.log(
69594
- `[engine] fast capture: frame ${frameIndex} \u2014 ${reason}; screenshot fallback for this frame (see fast-capture-limitations.md)`
69595
- );
69596
- const buffer = await pageScreenshotCapture(page, options);
69597
- return { encodeResult: Promise.resolve(buffer), captureTimeMs: Date.now() - startTime };
69598
- }
69599
69607
  if (session.isInitialized) {
69600
69608
  await captureFrameErrorDiagnostics(
69601
69609
  session,
@@ -69604,6 +69612,11 @@ async function captureFrameToBufferPipelined(session, frameIndex, time) {
69604
69612
  captureError instanceof Error ? captureError : new Error(String(captureError))
69605
69613
  );
69606
69614
  }
69615
+ if (isRecoverableDrawElementError(captureError)) {
69616
+ session.deNcprFallbacks = (session.deNcprFallbacks ?? 0) + 1;
69617
+ const reason = isCanvasNotInitializedError(captureError) ? "drawElement canvas not initialized" : "No cached paint record";
69618
+ throw new DrawElementCaptureError(frameIndex, reason, captureError);
69619
+ }
69607
69620
  throw captureError;
69608
69621
  }
69609
69622
  }
@@ -69637,6 +69650,8 @@ async function captureFramesBatchPipelined(session, frameIndices, times) {
69637
69650
  options.height,
69638
69651
  options.quality ?? 80
69639
69652
  );
69653
+ for (const result of encodeResults) void result.catch(() => {
69654
+ });
69640
69655
  const okCount = failedAt === null ? frameIndices.length : failedAt;
69641
69656
  const elapsed = Date.now() - startTime;
69642
69657
  session.capturePerf.frames += okCount;
@@ -69656,23 +69671,8 @@ async function captureFramesBatchPipelined(session, frameIndices, times) {
69656
69671
  if (failedAt !== null) {
69657
69672
  if (isRecoverableDrawElementError(error)) {
69658
69673
  const reason = isCanvasNotInitializedError(error) ? "drawElement canvas not initialized" : "No cached paint record";
69659
- console.log(
69660
- `[engine] fast capture: batch produce failed at frame ${frameIndices[failedAt] ?? "?"} (${reason}); screenshot fallback for ${frameIndices.length - failedAt} frame(s) (see fast-capture-limitations.md)`
69661
- );
69662
- for (let i2 = failedAt; i2 < frameIndices.length; i2++) {
69663
- const frameIndex = frameIndices[i2];
69664
- const time = times[i2];
69665
- if (frameIndex === void 0 || time === void 0) break;
69666
- session.deNcprFallbacks = (session.deNcprFallbacks ?? 0) + 1;
69667
- await prepareFrameForCapture(session, frameIndex, time);
69668
- const buffer = await pageScreenshotCapture(page, options);
69669
- const encodeResult = Promise.resolve(buffer);
69670
- if (session.staticFrames) {
69671
- session.lastEncodeResult = encodeResult;
69672
- session.lastEncodeResultFrame = frameIndex;
69673
- }
69674
- results.push({ frameIndex, encodeResult });
69675
- }
69674
+ session.deNcprFallbacks = (session.deNcprFallbacks ?? 0) + 1;
69675
+ throw new DrawElementCaptureError(frameIndices[failedAt] ?? failedAt, reason, error);
69676
69676
  } else {
69677
69677
  console.log(
69678
69678
  `[engine] fast capture: batch produce failed at frame ${frameIndices[failedAt] ?? "?"} (${error ?? "?"}); re-capturing ${frameIndices.length - failedAt} frame(s) per-frame`
@@ -69845,7 +69845,7 @@ function getCapturePerfSummary(session) {
69845
69845
  if (frames > 0 && ncprFallbacks / frames > DE_FALLBACK_RATIO_WARN_THRESHOLD) {
69846
69846
  const pct = Math.round(ncprFallbacks / frames * 100);
69847
69847
  console.warn(
69848
- `[engine] fast capture: ${ncprFallbacks}/${frames} frame(s) (${pct}%) fell back to screenshot capture (canvas-not-initialized / no-cached-paint-record) \u2014 drawElement likely failed to engage for this render rather than recovering a few edge-case frames; see fast-capture-limitations.md.`
69848
+ `[engine] fast capture: ${ncprFallbacks}/${frames} frame(s) (${pct}%) rejected due to missing canvas/paint records; fresh screenshot capture required.`
69849
69849
  );
69850
69850
  }
69851
69851
  return {
@@ -69895,6 +69895,7 @@ var init_frameCapture = __esm({
69895
69895
  "../engine/src/services/frameCapture.ts"() {
69896
69896
  "use strict";
69897
69897
  init_dist3();
69898
+ init_drawElementCaptureError();
69898
69899
  init_browserManager();
69899
69900
  init_screenshotService();
69900
69901
  init_drawElementService();
@@ -79752,6 +79753,21 @@ function findRootTag(source, parsedTags) {
79752
79753
  }
79753
79754
  return null;
79754
79755
  }
79756
+ function hasUnquotedLessThan(attrs2) {
79757
+ let quote = null;
79758
+ for (const ch of attrs2) {
79759
+ if (quote) {
79760
+ if (ch === quote) quote = null;
79761
+ continue;
79762
+ }
79763
+ if (ch === '"' || ch === "'") {
79764
+ quote = ch;
79765
+ } else if (ch === "<") {
79766
+ return true;
79767
+ }
79768
+ }
79769
+ return false;
79770
+ }
79755
79771
  function readAttr(tagSource, attr2) {
79756
79772
  if (!tagSource) return null;
79757
79773
  const escaped = attr2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -82765,6 +82781,21 @@ var init_dist4 = __esm({
82765
82781
  }
82766
82782
  return findings;
82767
82783
  },
82784
+ // unclosed_tag_swallowed_element
82785
+ ({ tags }) => {
82786
+ const findings = [];
82787
+ for (const tag of tags) {
82788
+ if (!hasUnquotedLessThan(tag.attrs)) continue;
82789
+ findings.push({
82790
+ code: "unclosed_tag_swallowed_element",
82791
+ severity: "error",
82792
+ message: `<${tag.name}> is missing its closing \`>\` before the next \`<\` \u2014 the following element is swallowed as bogus attribute text and never becomes a real node.`,
82793
+ fixHint: "Close the previous tag's `>` before opening the next element.",
82794
+ snippet: truncateSnippet(tag.raw)
82795
+ });
82796
+ }
82797
+ return findings;
82798
+ },
82768
82799
  // invalid_inline_script_syntax (malformed close tag)
82769
82800
  ({ source }) => {
82770
82801
  if (!INVALID_SCRIPT_CLOSE_PATTERN.test(source)) return [];
@@ -94476,6 +94507,7 @@ __export(src_exports, {
94476
94507
  DEFAULT_HDR10_MASTERING: () => DEFAULT_HDR10_MASTERING,
94477
94508
  DEFAULT_VP9_CPU_USED: () => DEFAULT_VP9_CPU_USED,
94478
94509
  DOM_LAYER_MASK_STYLE_ID: () => DOM_LAYER_MASK_STYLE_ID,
94510
+ DrawElementCaptureError: () => DrawElementCaptureError,
94479
94511
  DrawElementVerificationError: () => DrawElementVerificationError,
94480
94512
  ENABLE_BROWSER_POOL: () => ENABLE_BROWSER_POOL,
94481
94513
  ENCODER_PRESETS: () => ENCODER_PRESETS,
@@ -94590,6 +94622,7 @@ __export(src_exports, {
94590
94622
  initializeSession: () => initializeSession,
94591
94623
  injectVideoFramesBatch: () => injectVideoFramesBatch,
94592
94624
  isBlockedNetworkHost: () => isBlockedNetworkHost,
94625
+ isDrawElementCaptureError: () => isDrawElementCaptureError,
94593
94626
  isDrawElementVerificationError: () => isDrawElementVerificationError,
94594
94627
  isExternalFfmpegInterruption: () => isExternalFfmpegInterruption,
94595
94628
  isFatalCaptureFailure: () => isFatalCaptureFailure,
@@ -94705,6 +94738,7 @@ var init_src = __esm({
94705
94738
  init_videoFrameInjector();
94706
94739
  init_hdr();
94707
94740
  init_renderProvenance();
94741
+ init_drawElementCaptureError();
94708
94742
  }
94709
94743
  });
94710
94744
 
@@ -126935,7 +126969,7 @@ function revertedRouting(routing) {
126935
126969
  return freezeRouting({ ...routing, state: "reverted" });
126936
126970
  }
126937
126971
  function replanAfterFailure(plan2, failure) {
126938
- if (plan2.kind === "sdr_disk" && failure.kind === "draw_element_verification") {
126972
+ if (plan2.kind === "sdr_disk" && (failure.kind === "draw_element_verification" || failure.kind === "draw_element_capture")) {
126939
126973
  return createCapturePlan({
126940
126974
  ...plan2,
126941
126975
  forceScreenshot: true,
@@ -139692,7 +139726,7 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
139692
139726
  if (failure.kind === "cancelled") {
139693
139727
  throw error;
139694
139728
  }
139695
- if (isDrawElementVerificationError(error)) {
139729
+ if (isDrawElementVerificationError(error) || isDrawElementCaptureError(error)) {
139696
139730
  throw error;
139697
139731
  }
139698
139732
  const remaining = findMissingFrameRanges(
@@ -139920,7 +139954,7 @@ function resolveParallelRouterRetryPlan(args) {
139920
139954
  }
139921
139955
  function shouldRetryViaPinnedFallback(args) {
139922
139956
  if (args.isCancellation || args.isEncoderInterrupted) return false;
139923
- if (args.isVerifyError) return true;
139957
+ if (args.isVerifyError || args.isDeCaptureError) return true;
139924
139958
  if (args.isDeRendererStall === true || args.isSequentialCaptureStall === true) return true;
139925
139959
  return args.deWorkerInversion === "inverted" || args.deParallelRouter === "routed";
139926
139960
  }
@@ -139946,6 +139980,9 @@ async function closeOrphanedProbeForRetry(probe2, closer, log2, retryContext) {
139946
139980
  function shouldStreamParallelCapture(args) {
139947
139981
  return args.routerEnabled && args.workerCount > 1 && !args.useDrawElement && args.outputFormat === "mp4" && args.streamingOk && !args.layeredOrEffectRoute;
139948
139982
  }
139983
+ function shouldClampDefaultDrawElement(args) {
139984
+ return args.useDrawElement && !args.fastCaptureExplicitOptIn && (!args.useStreamingEncode || args.workerCount > 1) && !args.deParallelStreamVerified;
139985
+ }
139949
139986
  function resolveCaptureForceScreenshotForPageSideCompositing(args) {
139950
139987
  return args.usePageSideCompositing ? true : args.forceScreenshot;
139951
139988
  }
@@ -140688,6 +140725,32 @@ async function executeRenderPipeline(input2) {
140688
140725
  deWorkerInversion: deWorkerInversion ?? "none",
140689
140726
  deParallelRouter: deParallelRouter ?? "none"
140690
140727
  });
140728
+ let useStreamingEncode = shouldUseStreamingEncode(
140729
+ cfg,
140730
+ outputFormat,
140731
+ workerCount,
140732
+ job.duration,
140733
+ deParallelStreamForced
140734
+ );
140735
+ const deParallelStreamVerified = (deParallelStreamForced || process.env.HF_DE_PARALLEL_STREAM === "true") && useStreamingEncode && workerCount > 1;
140736
+ if (shouldClampDefaultDrawElement({
140737
+ useDrawElement: cfg.useDrawElement,
140738
+ fastCaptureExplicitOptIn: process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE === "true",
140739
+ useStreamingEncode,
140740
+ workerCount,
140741
+ deParallelStreamVerified
140742
+ })) {
140743
+ cfg.useDrawElement = false;
140744
+ deClampReason = workerCount > 1 ? "parallel" : "disk_path";
140745
+ log2.info(
140746
+ "[Render] Fast capture: default-on drawElement disabled for this render \u2014 " + (workerCount > 1 ? "parallel capture" : "the disk capture path") + " has no runtime self-verification. Set PRODUCER_EXPERIMENTAL_FAST_CAPTURE=true to override."
140747
+ );
140748
+ if (probeSession && probeSession.captureMode === "drawelement") {
140749
+ lastBrowserConsole = probeSession.browserConsoleBuffer;
140750
+ await closeCaptureSession(probeSession);
140751
+ probeSession = null;
140752
+ }
140753
+ }
140691
140754
  const captureParallelStreamRouterEnabled = process.env.HF_CAPTURE_PARALLEL_STREAM === "true";
140692
140755
  const captureParallelStreamArgs = {
140693
140756
  workerCount,
@@ -140719,7 +140782,7 @@ async function executeRenderPipeline(input2) {
140719
140782
  await closeCaptureSession(probeSession);
140720
140783
  probeSession = null;
140721
140784
  }
140722
- let useStreamingEncode = shouldUseStreamingEncode(
140785
+ useStreamingEncode = shouldUseStreamingEncode(
140723
140786
  cfg,
140724
140787
  outputFormat,
140725
140788
  workerCount,
@@ -140734,18 +140797,6 @@ async function executeRenderPipeline(input2) {
140734
140797
  durationSeconds: job.duration,
140735
140798
  maxDurationSeconds: cfg.streamingEncodeMaxDurationSeconds
140736
140799
  });
140737
- const deParallelStreamVerified = (deParallelStreamForced || process.env.HF_DE_PARALLEL_STREAM === "true") && useStreamingEncode && workerCount > 1;
140738
- if (cfg.useDrawElement && process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE !== "true" && (!useStreamingEncode || workerCount > 1) && !deParallelStreamVerified) {
140739
- cfg.useDrawElement = false;
140740
- deClampReason = workerCount > 1 ? "parallel" : "disk_path";
140741
- log2.info(
140742
- "[Render] Fast capture: default-on drawElement disabled for this render \u2014 " + (workerCount > 1 ? "parallel capture" : "the disk capture path") + " has no runtime self-verification. Set PRODUCER_EXPERIMENTAL_FAST_CAPTURE=true to override."
140743
- );
140744
- if (probeSession && probeSession.captureMode === "drawelement") {
140745
- await closeCaptureSession(probeSession);
140746
- probeSession = null;
140747
- }
140748
- }
140749
140800
  const FORMAT_EXT3 = {
140750
140801
  mp4: ".mp4",
140751
140802
  webm: ".webm",
@@ -141006,11 +141057,13 @@ async function executeRenderPipeline(input2) {
141006
141057
  streamingRes = await invokeStreaming();
141007
141058
  } catch (err) {
141008
141059
  const isVerifyError = isDrawElementVerificationError(err);
141060
+ const isDeCaptureError = isDrawElementCaptureError(err);
141009
141061
  const isDeStall = isDeRendererStallError(err);
141010
141062
  const isSequentialStall = isSequentialCaptureStallError(err);
141011
141063
  const isCancellation = err instanceof RenderCancelledError || executionSignal?.aborted === true;
141012
141064
  if (!shouldRetryViaPinnedFallback({
141013
141065
  isVerifyError,
141066
+ isDeCaptureError,
141014
141067
  isCancellation,
141015
141068
  isEncoderInterrupted: err instanceof EncoderInterruptedError,
141016
141069
  deWorkerInversion,
@@ -141031,12 +141084,12 @@ async function executeRenderPipeline(input2) {
141031
141084
  deFallbackReason = isMemoryExhaustion ? "oom" : isDeStall ? "de_renderer_stall" : "capture_error";
141032
141085
  }
141033
141086
  log2.warn(
141034
- isVerifyError ? "[Render] drawElement self-verification failed; re-rendering via screenshot" : isDeStall ? "[Render] drawElement renderer stalled; re-rendering via screenshot" : isSequentialStall ? "[Render] sequential capture stalled; retrying on a fresh screenshot session" : "[Render] capture failed on the pinned worker count; re-rendering via screenshot",
141087
+ isVerifyError ? "[Render] drawElement self-verification failed; re-rendering via screenshot" : isDeStall ? "[Render] drawElement renderer stalled; re-rendering via screenshot" : isSequentialStall ? "[Render] sequential capture stalled; retrying on a fresh screenshot session" : "[Render] capture failed; re-rendering via a fresh screenshot session",
141035
141088
  { error: err instanceof Error ? err.message : String(err) }
141036
141089
  );
141037
141090
  observability.checkpoint(
141038
141091
  "capture_streaming",
141039
- isVerifyError ? "drawElement self-verify failed; retrying with forceScreenshot" : isDeStall ? "drawElement renderer stalled; retrying with forceScreenshot" : isSequentialStall ? "sequential capture stalled; retrying with a fresh screenshot session" : "capture failed on pinned worker count; retrying with forceScreenshot"
141092
+ isVerifyError ? "drawElement self-verify failed; retrying with forceScreenshot" : isDeStall ? "drawElement renderer stalled; retrying with forceScreenshot" : isSequentialStall ? "sequential capture stalled; retrying with a fresh screenshot session" : "capture failed; retrying with a fresh screenshot session"
141040
141093
  );
141041
141094
  const failedRouting = capturePlan.routing.kind;
141042
141095
  capturePlan = replanAfterFailure(
@@ -141149,22 +141202,22 @@ async function executeRenderPipeline(input2) {
141149
141202
  try {
141150
141203
  captureRes = await invokeDiskCapture(capturePlan);
141151
141204
  } catch (err) {
141152
- if (!isDrawElementVerificationError(err) || err instanceof RenderCancelledError || executionSignal?.aborted === true) {
141205
+ if (!isDrawElementVerificationError(err) && !isDrawElementCaptureError(err) || err instanceof RenderCancelledError || executionSignal?.aborted === true) {
141153
141206
  throw err;
141154
141207
  }
141155
- deSelfVerifyFallback = true;
141208
+ deSelfVerifyFallback = isDrawElementVerificationError(err);
141156
141209
  const t2 = deVerifyFallbackTelemetry(err);
141157
- deFallbackReason = t2.reason;
141210
+ deFallbackReason = deSelfVerifyFallback ? t2.reason : "capture_error";
141158
141211
  deFallbackFailedDb = t2.failedDb;
141159
141212
  deFallbackFrameIndex = t2.frameIndex;
141160
141213
  deFallbackThresholdDb = t2.thresholdDb;
141161
141214
  log2.warn(
141162
- "[Render] drawElement self-verification failed on the parallel disk path; re-rendering via screenshot",
141215
+ "[Render] drawElement capture failed on the parallel disk path; re-rendering via screenshot",
141163
141216
  { error: err instanceof Error ? err.message : String(err) }
141164
141217
  );
141165
141218
  observability.checkpoint(
141166
141219
  "capture_disk",
141167
- "drawElement self-verify failed; retrying with forceScreenshot"
141220
+ "drawElement capture failed; retrying with a fresh screenshot session"
141168
141221
  );
141169
141222
  rmSync23(framesDir, { recursive: true, force: true });
141170
141223
  mkdirSync27(framesDir, { recursive: true });
@@ -141177,7 +141230,9 @@ async function executeRenderPipeline(input2) {
141177
141230
  probeSession = null;
141178
141231
  await closeOrphanedProbeForRetry(orphaned, closeCaptureSession, log2, "disk verify");
141179
141232
  }
141180
- capturePlan = replanAfterFailure(capturePlan, { kind: "draw_element_verification" });
141233
+ capturePlan = replanAfterFailure(capturePlan, {
141234
+ kind: deSelfVerifyFallback ? "draw_element_verification" : "draw_element_capture"
141235
+ });
141181
141236
  syncCapturePlan();
141182
141237
  updateCaptureObservability({
141183
141238
  forceScreenshot: capturePlan.forceScreenshot,
@@ -150848,10 +150903,10 @@ function searchMissCommand(query2, tier) {
150848
150903
  const quoted = query2.replace(/(["\\$`])/g, "\\$1");
150849
150904
  return `npx hyperframes feedback --search-miss "${quoted}" --wanted "<the move you needed>" --tier ${tier}`;
150850
150905
  }
150851
- function reportLocalModelOption(json) {
150852
- if (!json && process.stdout.isTTY) return;
150853
- if (localModelStatus().status !== "not-asked") return;
150854
- console.error(nonInteractiveConsentMessage());
150906
+ function localModelHint(json, status) {
150907
+ if (!json && process.stdout.isTTY) return null;
150908
+ if (status?.status !== "not-asked") return null;
150909
+ return nonInteractiveConsentMessage();
150855
150910
  }
150856
150911
  async function offerLocalModel(matchCount, json, registryBaseUrl, artifactRevision) {
150857
150912
  if (json || !process.stdout.isTTY) return;
@@ -150982,6 +151037,11 @@ var init_catalog = __esm({
150982
151037
  const matching = searched ? searched.items : tagged;
150983
151038
  if (matching.length === 0) {
150984
151039
  const unsearchable = Boolean(query2) && searched?.localMode === "words" && hasNoSearchableTokens(query2);
151040
+ const tierHint = query2 && !unsearchable && warnings.length === 0 ? localModelHint(json, effectiveStatus) : null;
151041
+ if (tierHint) {
151042
+ warnings.push(tierHint);
151043
+ console.error(tierHint);
151044
+ }
150985
151045
  if (json && query2) {
150986
151046
  console.log(
150987
151047
  JSON.stringify(
@@ -151087,7 +151147,8 @@ var init_catalog = __esm({
151087
151147
  }
151088
151148
  if (searched.localMode === "words") {
151089
151149
  if (warnings.length === 0) {
151090
- reportLocalModelOption(json);
151150
+ const hint2 = localModelHint(json, effectiveStatus);
151151
+ if (hint2) console.error(hint2);
151091
151152
  await offerLocalModel(matching.length, json, config.registry, artifactRevision);
151092
151153
  }
151093
151154
  }
@@ -153388,7 +153449,7 @@ function uploadTimeoutMs(byteLength) {
153388
153449
  );
153389
153450
  }
153390
153451
  function shouldIgnoreSegment(segment) {
153391
- return segment.startsWith(".") || IGNORED_DIRS.has(segment) || IGNORED_FILES.has(segment);
153452
+ return IGNORED_DIRS.has(segment) || IGNORED_FILES.has(segment);
153392
153453
  }
153393
153454
  function createProjectIgnore(rootDir) {
153394
153455
  const matcher = ignore2().add(DEFAULT_PROJECT_IGNORE);
@@ -153714,7 +153775,7 @@ var init_publishProject = __esm({
153714
153775
  IGNORED_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", "dist", ".next", "coverage"]);
153715
153776
  IGNORED_FILES = /* @__PURE__ */ new Set([".DS_Store", "Thumbs.db"]);
153716
153777
  HYPERFRAMES_IGNORE_FILE = ".hyperframesignore";
153717
- DEFAULT_PROJECT_IGNORE = ["/renders/", "/snapshots/"];
153778
+ DEFAULT_PROJECT_IGNORE = ["/renders/", "/snapshots/", ".*"];
153718
153779
  PUBLISH_CONTENT_TYPE = "application/zip";
153719
153780
  PUBLISH_METADATA_TIMEOUT_MS = 3e4;
153720
153781
  PUBLISH_UPLOAD_MIN_TIMEOUT_MS = 12e4;
@@ -164939,7 +165000,7 @@ function createKeyframesCommand(options = {}) {
164939
165000
  },
164940
165001
  layout: {
164941
165002
  type: "string",
164942
- description: "--shot layout: 'path' (ghosts at real positions + path, default) or 'strip' (filmstrip by time \u2014 for in-place/overlapping motion)."
165003
+ description: "--shot layout: 'path' (ghosts at real positions + path, default) or 'strip' (for in-place/overlapping motion). 'strip' captures a real per-time pixel filmstrip only when --selector targets an SVG element; any other selector (e.g. a DOM/sub-composition host) instead gets one live frame plus vector position markers."
164943
165004
  },
164944
165005
  from: { type: "string", description: "--shot: sample only from this time (seconds)." },
164945
165006
  to: { type: "string", description: "--shot: sample only up to this time (seconds)." },
@@ -166446,19 +166507,25 @@ var init_manager4 = __esm({
166446
166507
 
166447
166508
  // src/tts/python.ts
166448
166509
  import { execFileSync as execFileSync14 } from "child_process";
166510
+ function validatePythonOverride(override) {
166511
+ try {
166512
+ const version2 = execFileSync14(override, ["--version"], {
166513
+ encoding: "utf-8",
166514
+ stdio: ["pipe", "pipe", "pipe"],
166515
+ timeout: 5e3
166516
+ });
166517
+ if (/Python 3/.test(version2)) return { ok: true };
166518
+ return {
166519
+ ok: false,
166520
+ reason: `did not report a Python 3 version (got ${JSON.stringify(version2.trim())})`
166521
+ };
166522
+ } catch (error) {
166523
+ return { ok: false, reason: error instanceof Error ? error.message : String(error) };
166524
+ }
166525
+ }
166449
166526
  function findPython() {
166450
166527
  const override = process.env.HYPERFRAMES_PYTHON;
166451
- if (override) {
166452
- try {
166453
- const version2 = execFileSync14(override, ["--version"], {
166454
- encoding: "utf-8",
166455
- stdio: ["pipe", "pipe", "pipe"],
166456
- timeout: 5e3
166457
- });
166458
- if (/Python 3/.test(version2)) return override;
166459
- } catch {
166460
- }
166461
- }
166528
+ if (override && validatePythonOverride(override).ok) return override;
166462
166529
  for (const name of ["python3", "python"]) {
166463
166530
  try {
166464
166531
  const cmd = process.platform === "win32" ? "where" : "which";
@@ -166480,6 +166547,13 @@ function findPython() {
166480
166547
  }
166481
166548
  return void 0;
166482
166549
  }
166550
+ function describeRejectedPythonOverride() {
166551
+ const override = process.env.HYPERFRAMES_PYTHON;
166552
+ if (!override) return null;
166553
+ const validation = validatePythonOverride(override);
166554
+ if (validation.ok) return null;
166555
+ return `HYPERFRAMES_PYTHON="${override}" was rejected: ${validation.reason}`;
166556
+ }
166483
166557
  function hasPythonPackage(python, pkg) {
166484
166558
  try {
166485
166559
  execFileSync14(python, ["-c", `import ${pkg}`], {
@@ -167260,11 +167334,15 @@ async function checkWhisper() {
167260
167334
  hint: getInstallInstructions2()
167261
167335
  };
167262
167336
  }
167337
+ function notInstalledDetail(base2) {
167338
+ const overrideRejection = describeRejectedPythonOverride();
167339
+ return overrideRejection ? `${base2}. ${overrideRejection}` : base2;
167340
+ }
167263
167341
  function checkLocalVoice() {
167264
167342
  if (hasPythonModules(KOKORO_MODULES)) return { ok: true, detail: "Kokoro deps installed" };
167265
167343
  return {
167266
167344
  ok: false,
167267
- detail: "Not installed (optional \u2014 local voice fallback)",
167345
+ detail: notInstalledDetail("Not installed (optional \u2014 local voice fallback)"),
167268
167346
  hint: KOKORO_PIP
167269
167347
  };
167270
167348
  }
@@ -167272,7 +167350,7 @@ function checkLocalMusic() {
167272
167350
  if (hasPythonModules(MUSICGEN_MODULES)) return { ok: true, detail: "MusicGen deps installed" };
167273
167351
  return {
167274
167352
  ok: false,
167275
- detail: "Not installed (optional \u2014 local music fallback)",
167353
+ detail: notInstalledDetail("Not installed (optional \u2014 local music fallback)"),
167276
167354
  hint: MUSICGEN_PIP
167277
167355
  };
167278
167356
  }
@@ -1689,7 +1689,13 @@
1689
1689
  axes && axes !== "normal" ? axes : ""
1690
1690
  }`;
1691
1691
  });
1692
- for (const media of root.querySelectorAll("canvas, video")) {
1692
+ // img shares the same pixel-only-motion blind spot as canvas/video: an
1693
+ // equal-size, equal-position opaque src/visibility swap moves no
1694
+ // geometry and no opacity. mediaPixelHash already handles it generically
1695
+ // (drawImage accepts any CanvasImageSource; width/height fall back to
1696
+ // rect.width/height same as canvas/video), so only the element selector
1697
+ // needed widening.
1698
+ for (const media of root.querySelectorAll("canvas, video, img")) {
1693
1699
  if (!isVisibleElement(media)) continue;
1694
1700
  parts.push(`p:${mediaPixelHash(media)}`);
1695
1701
  }