@hyperframes/producer 0.8.197 → 0.8.198

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/index.js CHANGED
@@ -14571,6 +14571,70 @@ async function measurePlanSecondsPerFrame(input) {
14571
14571
  }
14572
14572
  }
14573
14573
 
14574
+ // src/services/distributed/chunkWeights.ts
14575
+ import { resolveFrameFormat } from "@hyperframes/engine";
14576
+ var PAGE_BASE_WEIGHT = 0.7;
14577
+ var PNG_WEIGHT_FACTOR = 1.5;
14578
+ function isUniformFrameCost(weights) {
14579
+ let lightest = Infinity;
14580
+ let heaviest = 0;
14581
+ for (const weight of weights) {
14582
+ lightest = Math.min(lightest, weight);
14583
+ heaviest = Math.max(heaviest, weight);
14584
+ }
14585
+ return heaviest <= lightest * 1.05;
14586
+ }
14587
+ function planFrameWeights(input) {
14588
+ const { totalFrames, fps } = input;
14589
+ const delta = new Float64Array(totalFrames + 1);
14590
+ const outputPixels = input.width * input.height;
14591
+ const extractedById = new Map(input.extracted.map((entry) => [entry.videoId, entry]));
14592
+ for (const video of input.videos) {
14593
+ const entry = extractedById.get(video.id);
14594
+ if (!entry || video.hidden) continue;
14595
+ const start = Math.max(0, Math.round(video.start * fps));
14596
+ const end = Math.min(totalFrames, Math.round(video.end * fps));
14597
+ if (!(end > start)) continue;
14598
+ const { width, height } = entry.metadata;
14599
+ const factor = resolveFrameFormat(entry.metadata) === "png" ? PNG_WEIGHT_FACTOR : 1;
14600
+ const weight = Math.min(width * height, outputPixels) / 1e6 * factor;
14601
+ delta[start] = delta[start] + weight;
14602
+ delta[end] = delta[end] - weight;
14603
+ }
14604
+ const weights = new Float64Array(totalFrames);
14605
+ let active = 0;
14606
+ for (let frame = 0; frame < totalFrames; frame++) {
14607
+ active += delta[frame];
14608
+ weights[frame] = PAGE_BASE_WEIGHT + active;
14609
+ }
14610
+ return weights;
14611
+ }
14612
+ function buildWeightedChunkSlices(weights, chunkCount, minFrames, maxFrames) {
14613
+ if (isUniformFrameCost(weights)) return null;
14614
+ const totalFrames = weights.length;
14615
+ const total = weights.reduce((sum2, weight) => sum2 + weight, 0);
14616
+ const minSize = Math.max(1, Math.min(minFrames, Math.floor(totalFrames / chunkCount)));
14617
+ const maxSize = Math.max(maxFrames, Math.ceil(totalFrames / chunkCount));
14618
+ const slices = [];
14619
+ let startFrame = 0;
14620
+ let frame = 0;
14621
+ let costBefore = 0;
14622
+ for (let index = 0; index < chunkCount - 1; index++) {
14623
+ const target = (index + 1) * total / chunkCount;
14624
+ while (frame < totalFrames && costBefore + weights[frame] / 2 < target) {
14625
+ costBefore += weights[frame++];
14626
+ }
14627
+ const remaining = chunkCount - index - 1;
14628
+ const low = Math.max(startFrame + minSize, totalFrames - remaining * maxSize);
14629
+ const high = Math.min(startFrame + maxSize, totalFrames - remaining * minSize);
14630
+ const endFrame = Math.min(high, Math.max(low, frame));
14631
+ slices.push({ index, startFrame, endFrame });
14632
+ startFrame = endFrame;
14633
+ }
14634
+ slices.push({ index: chunkCount - 1, startFrame, endFrame: totalFrames });
14635
+ return slices;
14636
+ }
14637
+
14574
14638
  // src/services/render/stages/freezePlan.ts
14575
14639
  import { existsSync as existsSync19, mkdirSync as mkdirSync10, readFileSync as readFileSync10, readdirSync as readdirSync7, writeFileSync as writeFileSync6 } from "node:fs";
14576
14640
  import { join as join29, relative as relative3, resolve as resolve8 } from "node:path";
@@ -15685,6 +15749,7 @@ var PLAN_PROJECT_DIR_SKIP_SEGMENTS = /* @__PURE__ */ new Set([
15685
15749
  ]);
15686
15750
  var DEFAULT_MAX_PARALLEL_CHUNKS = 16;
15687
15751
  var MIN_CHUNK_SIZE = 10;
15752
+ var MAX_CHUNK_SIZE = 3600;
15688
15753
  var MAX_PARALLEL_CHUNKS_CEILING = 256;
15689
15754
  var ESTIMATED_SECONDS_PER_MEGAPIXEL_FRAME = 0.11;
15690
15755
  var ESTIMATED_CHUNK_OVERHEAD_SECONDS = 60;
@@ -15711,6 +15776,11 @@ var ChunkExceedsWorkerTimeoutError = class extends Error {
15711
15776
  this.name = "ChunkExceedsWorkerTimeoutError";
15712
15777
  }
15713
15778
  };
15779
+ function framesWithinWorkerTimeout(workerTimeoutSeconds, secondsPerFrame) {
15780
+ return Math.floor(
15781
+ (workerTimeoutSeconds * WORKER_TIMEOUT_MARGIN - ESTIMATED_CHUNK_OVERHEAD_SECONDS) / secondsPerFrame
15782
+ );
15783
+ }
15714
15784
  function resolveMaxParallelChunks(config, totalFrames, megapixels, log = defaultLogger, measuredSecondsPerFrame) {
15715
15785
  if (config.maxParallelChunks !== void 0) return config.maxParallelChunks;
15716
15786
  const { workerTimeoutSeconds, workerConcurrency } = config;
@@ -15723,9 +15793,7 @@ function resolveMaxParallelChunks(config, totalFrames, megapixels, log = default
15723
15793
  const minFramesPerChunk = Math.ceil(ESTIMATED_CHUNK_OVERHEAD_SECONDS / secondsPerFrame);
15724
15794
  const wide = Math.max(1, Math.min(wave, Math.floor(totalFrames / minFramesPerChunk)));
15725
15795
  if (workerTimeoutSeconds === void 0) return wide;
15726
- const maxFramesPerChunk = Math.floor(
15727
- (workerTimeoutSeconds * WORKER_TIMEOUT_MARGIN - ESTIMATED_CHUNK_OVERHEAD_SECONDS) / secondsPerFrame
15728
- );
15796
+ const maxFramesPerChunk = framesWithinWorkerTimeout(workerTimeoutSeconds, secondsPerFrame);
15729
15797
  const needed = maxFramesPerChunk < 1 ? Infinity : Math.ceil(totalFrames / maxFramesPerChunk);
15730
15798
  if (needed > MAX_PARALLEL_CHUNKS_CEILING) {
15731
15799
  const framesAtCeiling = Math.ceil(totalFrames / MAX_PARALLEL_CHUNKS_CEILING);
@@ -15854,6 +15922,15 @@ function buildChunkSlices(totalFrames, chunkCount, effectiveChunkSize) {
15854
15922
  }
15855
15923
  return slices;
15856
15924
  }
15925
+ function buildPlanChunkSlices(frameWeights, config, chunkCount, effectiveChunkSize, maxFramesPerChunk = Infinity) {
15926
+ const weighted = config.chunkSize === void 0 ? buildWeightedChunkSlices(
15927
+ frameWeights,
15928
+ chunkCount,
15929
+ MIN_CHUNK_SIZE,
15930
+ Math.min(config.targetChunkFrames ?? MAX_CHUNK_SIZE, MAX_CHUNK_SIZE, maxFramesPerChunk)
15931
+ ) : null;
15932
+ return weighted ?? buildChunkSlices(frameWeights.length, chunkCount, effectiveChunkSize);
15933
+ }
15857
15934
  async function readFontSnapshotSha() {
15858
15935
  const module = await import("@hyperframes/core/fonts/embedded-data");
15859
15936
  const data = module.EMBEDDED_FONT_DATA;
@@ -16303,6 +16380,14 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
16303
16380
  const digests = options.digests ?? /* @__PURE__ */ new Map();
16304
16381
  const separateAudio = plansSeparateAudio(config, options);
16305
16382
  const chunksCarryAudio = !separateAudio && carriesChunkAudio(config, options);
16383
+ const frameWeights = planFrameWeights({
16384
+ videos: planVideosJson.videos,
16385
+ extracted: planVideosJson.extracted,
16386
+ totalFrames,
16387
+ fps: config.fps,
16388
+ width,
16389
+ height
16390
+ });
16306
16391
  stageStarted = Date.now();
16307
16392
  const [audioOutcome, overlapOutcome, frameCostOutcome] = await Promise.allSettled([
16308
16393
  separateAudio ? null : mixPlanAudio(c, finalCompiledDir).finally(() => logPlanStage(log, "audio", stageStarted)),
@@ -16353,7 +16438,16 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
16353
16438
  maxParallel,
16354
16439
  config.targetChunkFrames
16355
16440
  );
16356
- const chunks = buildChunkSlices(totalFrames, chunkCount, effectiveChunkSize);
16441
+ const chunks = buildPlanChunkSlices(
16442
+ frameWeights,
16443
+ config,
16444
+ chunkCount,
16445
+ effectiveChunkSize,
16446
+ chunkSizingUsesFrameCost(config) && config.workerTimeoutSeconds !== void 0 ? framesWithinWorkerTimeout(
16447
+ config.workerTimeoutSeconds,
16448
+ estimateFrameSeconds(megapixels, frameCostOutcome.value)
16449
+ ) : void 0
16450
+ );
16357
16451
  const audioResult = audioOutcome.value;
16358
16452
  if (audioResult?.audioError) {
16359
16453
  applyDistributedAudioWarningPolicy(job, audioResult.audioError, audioResult.audioFailures, log);
@@ -16499,7 +16593,6 @@ var ALLOWED_RUNTIME_CAPS = ["lambda", "temporal", "cloud-run-job", "k8s-job", "n
16499
16593
  var ALLOWED_HDR_MODES = ["auto", "force-sdr"];
16500
16594
  var MAX_DIMENSION = 7680;
16501
16595
  var MIN_DIMENSION = 16;
16502
- var MAX_CHUNK_SIZE = 3600;
16503
16596
  function validateDistributedRenderConfig(config) {
16504
16597
  if (config === null || typeof config !== "object") {
16505
16598
  throw new InvalidConfigError("config", "must be an object");
@@ -16992,6 +17085,7 @@ import crypto from "node:crypto";
16992
17085
  import { Hono as Hono2 } from "hono";
16993
17086
  import { streamSSE } from "hono/streaming";
16994
17087
  import { serve as serve2 } from "@hono/node-server";
17088
+ import { fetchPublicHttpsText as fetchPublicHttpsText2 } from "@hyperframes/engine";
16995
17089
 
16996
17090
  // src/services/hyperframeLint.ts
16997
17091
  import {
@@ -17374,6 +17468,7 @@ function validateOutputResolutionOverride(body) {
17374
17468
  }
17375
17469
  return void 0;
17376
17470
  }
17471
+ var MAX_PREVIEW_HTML_BYTES = 20 * 1024 * 1024;
17377
17472
  function prepareProjectDirectory(projectDir, options) {
17378
17473
  const candidate = nonEmptyString(projectDir);
17379
17474
  if (!candidate) return null;
@@ -17394,11 +17489,7 @@ async function resolveInlineRenderHtml(body) {
17394
17489
  if (!previewUrl)
17395
17490
  return { error: "Missing render source: provide projectDir, previewUrl, or html" };
17396
17491
  try {
17397
- const response = await fetch(previewUrl, { method: "GET" });
17398
- if (!response.ok) {
17399
- return { error: `Failed to fetch previewUrl: ${response.status} ${response.statusText}` };
17400
- }
17401
- return { html: await response.text() };
17492
+ return { html: await fetchPublicHttpsText2(previewUrl, { maxBytes: MAX_PREVIEW_HTML_BYTES }) };
17402
17493
  } catch (error) {
17403
17494
  return {
17404
17495
  error: `Failed to fetch previewUrl: ${error instanceof Error ? error.message : String(error)}`
@@ -17,6 +17,7 @@ import crypto from "node:crypto";
17
17
  import { Hono as Hono2 } from "hono";
18
18
  import { streamSSE } from "hono/streaming";
19
19
  import { serve as serve2 } from "@hono/node-server";
20
+ import { fetchPublicHttpsText as fetchPublicHttpsText2 } from "@hyperframes/engine";
20
21
 
21
22
  // src/services/renderOrchestrator.ts
22
23
  import { audioFailureDiagnostics as audioFailureDiagnostics2 } from "@hyperframes/engine";
@@ -14233,6 +14234,9 @@ import {
14233
14234
  var PLAN_AUDIO_RELATIVE_PATH = MIXED_AUDIO_FILENAME3;
14234
14235
  var execFile = promisify2(execFileCallback);
14235
14236
 
14237
+ // src/services/distributed/chunkWeights.ts
14238
+ import { resolveFrameFormat } from "@hyperframes/engine";
14239
+
14236
14240
  // src/services/distributed/planProtocol.ts
14237
14241
  var PLAN_SCHEMA_VERSION = 1;
14238
14242
  var PLAN_ARTIFACT_LAYOUT = "plan-dir-v1";
@@ -14689,6 +14693,7 @@ function validateOutputResolutionOverride(body) {
14689
14693
  }
14690
14694
  return void 0;
14691
14695
  }
14696
+ var MAX_PREVIEW_HTML_BYTES = 20 * 1024 * 1024;
14692
14697
  function prepareProjectDirectory(projectDir, options) {
14693
14698
  const candidate = nonEmptyString(projectDir);
14694
14699
  if (!candidate) return null;
@@ -14709,11 +14714,7 @@ async function resolveInlineRenderHtml(body) {
14709
14714
  if (!previewUrl)
14710
14715
  return { error: "Missing render source: provide projectDir, previewUrl, or html" };
14711
14716
  try {
14712
- const response = await fetch(previewUrl, { method: "GET" });
14713
- if (!response.ok) {
14714
- return { error: `Failed to fetch previewUrl: ${response.status} ${response.statusText}` };
14715
- }
14716
- return { html: await response.text() };
14717
+ return { html: await fetchPublicHttpsText2(previewUrl, { maxBytes: MAX_PREVIEW_HTML_BYTES }) };
14717
14718
  } catch (error) {
14718
14719
  return {
14719
14720
  error: `Failed to fetch previewUrl: ${error instanceof Error ? error.message : String(error)}`
@@ -0,0 +1,16 @@
1
+ import type { ChunkSliceJson } from "../render/stages/freezePlan.js";
2
+ import type { PlanVideosJson } from "./shared.js";
3
+ /** Estimated capture cost of every frame: page base plus each active video's pixels. */
4
+ export declare function planFrameWeights(input: {
5
+ videos: PlanVideosJson["videos"];
6
+ extracted: PlanVideosJson["extracted"];
7
+ totalFrames: number;
8
+ fps: number;
9
+ width: number;
10
+ height: number;
11
+ }): Float64Array;
12
+ /**
13
+ * Cut `[0, weights.length)` into `chunkCount` ranges of ~equal summed weight, each within
14
+ * `[minFrames, maxFrames]` where the frame count allows. `null` when the cost is uniform.
15
+ */
16
+ export declare function buildWeightedChunkSlices(weights: Float64Array, chunkCount: number, minFrames: number, maxFrames: number): ChunkSliceJson[] | null;
@@ -271,6 +271,8 @@ export declare const DEFAULT_MAX_PARALLEL_CHUNKS = 16;
271
271
  * outweighs the parallelism gain on tiny renders.
272
272
  */
273
273
  export declare const MIN_CHUNK_SIZE = 10;
274
+ /** Largest `chunkSize`; `validateDistributedRenderConfig` enforces it, and cost-weighted chunks stay under it. */
275
+ export declare const MAX_CHUNK_SIZE = 3600;
274
276
  /** Most chunks one render may plan; `validateDistributedRenderConfig` enforces it on `maxParallelChunks`. */
275
277
  export declare const MAX_PARALLEL_CHUNKS_CEILING = 256;
276
278
  /** Predicted seconds for one chunk of `frames` frames at `megapixels` output pixels per frame. */
@@ -402,6 +404,11 @@ export declare function resolveChunkPlan(totalFrames: number, configChunkSize: n
402
404
  * upper bound — chunk workers iterate `i in [startFrame, endFrame)`.
403
405
  */
404
406
  export declare function buildChunkSlices(totalFrames: number, chunkCount: number, effectiveChunkSize: number): ChunkSliceJson[];
407
+ /**
408
+ * The plan's chunk cut: auto-sized plans re-cut the same chunk count by estimated capture cost;
409
+ * explicit `chunkSize` or uniform cost keeps {@link buildChunkSlices}; `targetChunkFrames` and the time limit cap it.
410
+ */
411
+ export declare function buildPlanChunkSlices(frameWeights: Float64Array, config: Pick<DistributedRenderConfig, "chunkSize" | "targetChunkFrames">, chunkCount: number, effectiveChunkSize: number, maxFramesPerChunk?: number): ChunkSliceJson[];
405
412
  /** Test-visible construction of the engine config used by distributed planning. */
406
413
  export declare function resolveDistributedEngineConfig(config: DistributedRenderConfig): EngineConfig;
407
414
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/producer",
3
- "version": "0.8.197",
3
+ "version": "0.8.198",
4
4
  "description": "HTML-to-video rendering engine using Chrome's BeginFrame API",
5
5
  "repository": {
6
6
  "type": "git",
@@ -35,11 +35,11 @@
35
35
  "postcss": "^8.4.0",
36
36
  "puppeteer": "^25.2.1",
37
37
  "puppeteer-core": "^25.2.1",
38
- "@hyperframes/engine": "0.8.197",
39
- "@hyperframes/lint": "0.8.197",
40
- "@hyperframes/core": "0.8.197",
41
- "@hyperframes/parsers": "0.8.197",
42
- "@hyperframes/studio-server": "0.8.197"
38
+ "@hyperframes/core": "0.8.198",
39
+ "@hyperframes/engine": "0.8.198",
40
+ "@hyperframes/lint": "0.8.198",
41
+ "@hyperframes/parsers": "0.8.198",
42
+ "@hyperframes/studio-server": "0.8.198"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@types/node": "^25.0.10",
@@ -48,7 +48,7 @@
48
48
  "tsx": "^4.21.0",
49
49
  "typescript": "^5.7.2",
50
50
  "vitest": "^4.1.11",
51
- "@hyperframes/aws-lambda": "0.8.197"
51
+ "@hyperframes/aws-lambda": "0.8.198"
52
52
  },
53
53
  "engines": {
54
54
  "node": ">=22"