@hyperframes/producer 0.8.196 → 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
@@ -14171,6 +14171,10 @@ function readVideoMetadata(value, field) {
14171
14171
  hasAudio: readBoolean(record.hasAudio, `${field}.hasAudio`),
14172
14172
  isVFR: readBoolean(record.isVFR, `${field}.isVFR`),
14173
14173
  hasAlpha: readBoolean(record.hasAlpha, `${field}.hasAlpha`),
14174
+ // Optional: plans from producers without the decode probe keep today's png for vp8/vp9.
14175
+ ...record.alphaPlaneDecoded === void 0 ? {} : {
14176
+ alphaPlaneDecoded: readBoolean(record.alphaPlaneDecoded, `${field}.alphaPlaneDecoded`)
14177
+ },
14174
14178
  colorSpace
14175
14179
  };
14176
14180
  }
@@ -14567,6 +14571,70 @@ async function measurePlanSecondsPerFrame(input) {
14567
14571
  }
14568
14572
  }
14569
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
+
14570
14638
  // src/services/render/stages/freezePlan.ts
14571
14639
  import { existsSync as existsSync19, mkdirSync as mkdirSync10, readFileSync as readFileSync10, readdirSync as readdirSync7, writeFileSync as writeFileSync6 } from "node:fs";
14572
14640
  import { join as join29, relative as relative3, resolve as resolve8 } from "node:path";
@@ -15681,6 +15749,7 @@ var PLAN_PROJECT_DIR_SKIP_SEGMENTS = /* @__PURE__ */ new Set([
15681
15749
  ]);
15682
15750
  var DEFAULT_MAX_PARALLEL_CHUNKS = 16;
15683
15751
  var MIN_CHUNK_SIZE = 10;
15752
+ var MAX_CHUNK_SIZE = 3600;
15684
15753
  var MAX_PARALLEL_CHUNKS_CEILING = 256;
15685
15754
  var ESTIMATED_SECONDS_PER_MEGAPIXEL_FRAME = 0.11;
15686
15755
  var ESTIMATED_CHUNK_OVERHEAD_SECONDS = 60;
@@ -15707,6 +15776,11 @@ var ChunkExceedsWorkerTimeoutError = class extends Error {
15707
15776
  this.name = "ChunkExceedsWorkerTimeoutError";
15708
15777
  }
15709
15778
  };
15779
+ function framesWithinWorkerTimeout(workerTimeoutSeconds, secondsPerFrame) {
15780
+ return Math.floor(
15781
+ (workerTimeoutSeconds * WORKER_TIMEOUT_MARGIN - ESTIMATED_CHUNK_OVERHEAD_SECONDS) / secondsPerFrame
15782
+ );
15783
+ }
15710
15784
  function resolveMaxParallelChunks(config, totalFrames, megapixels, log = defaultLogger, measuredSecondsPerFrame) {
15711
15785
  if (config.maxParallelChunks !== void 0) return config.maxParallelChunks;
15712
15786
  const { workerTimeoutSeconds, workerConcurrency } = config;
@@ -15719,9 +15793,7 @@ function resolveMaxParallelChunks(config, totalFrames, megapixels, log = default
15719
15793
  const minFramesPerChunk = Math.ceil(ESTIMATED_CHUNK_OVERHEAD_SECONDS / secondsPerFrame);
15720
15794
  const wide = Math.max(1, Math.min(wave, Math.floor(totalFrames / minFramesPerChunk)));
15721
15795
  if (workerTimeoutSeconds === void 0) return wide;
15722
- const maxFramesPerChunk = Math.floor(
15723
- (workerTimeoutSeconds * WORKER_TIMEOUT_MARGIN - ESTIMATED_CHUNK_OVERHEAD_SECONDS) / secondsPerFrame
15724
- );
15796
+ const maxFramesPerChunk = framesWithinWorkerTimeout(workerTimeoutSeconds, secondsPerFrame);
15725
15797
  const needed = maxFramesPerChunk < 1 ? Infinity : Math.ceil(totalFrames / maxFramesPerChunk);
15726
15798
  if (needed > MAX_PARALLEL_CHUNKS_CEILING) {
15727
15799
  const framesAtCeiling = Math.ceil(totalFrames / MAX_PARALLEL_CHUNKS_CEILING);
@@ -15850,6 +15922,15 @@ function buildChunkSlices(totalFrames, chunkCount, effectiveChunkSize) {
15850
15922
  }
15851
15923
  return slices;
15852
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
+ }
15853
15934
  async function readFontSnapshotSha() {
15854
15935
  const module = await import("@hyperframes/core/fonts/embedded-data");
15855
15936
  const data = module.EMBEDDED_FONT_DATA;
@@ -16299,6 +16380,14 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
16299
16380
  const digests = options.digests ?? /* @__PURE__ */ new Map();
16300
16381
  const separateAudio = plansSeparateAudio(config, options);
16301
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
+ });
16302
16391
  stageStarted = Date.now();
16303
16392
  const [audioOutcome, overlapOutcome, frameCostOutcome] = await Promise.allSettled([
16304
16393
  separateAudio ? null : mixPlanAudio(c, finalCompiledDir).finally(() => logPlanStage(log, "audio", stageStarted)),
@@ -16349,7 +16438,16 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
16349
16438
  maxParallel,
16350
16439
  config.targetChunkFrames
16351
16440
  );
16352
- 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
+ );
16353
16451
  const audioResult = audioOutcome.value;
16354
16452
  if (audioResult?.audioError) {
16355
16453
  applyDistributedAudioWarningPolicy(job, audioResult.audioError, audioResult.audioFailures, log);
@@ -16495,7 +16593,6 @@ var ALLOWED_RUNTIME_CAPS = ["lambda", "temporal", "cloud-run-job", "k8s-job", "n
16495
16593
  var ALLOWED_HDR_MODES = ["auto", "force-sdr"];
16496
16594
  var MAX_DIMENSION = 7680;
16497
16595
  var MIN_DIMENSION = 16;
16498
- var MAX_CHUNK_SIZE = 3600;
16499
16596
  function validateDistributedRenderConfig(config) {
16500
16597
  if (config === null || typeof config !== "object") {
16501
16598
  throw new InvalidConfigError("config", "must be an object");
@@ -16988,6 +17085,7 @@ import crypto from "node:crypto";
16988
17085
  import { Hono as Hono2 } from "hono";
16989
17086
  import { streamSSE } from "hono/streaming";
16990
17087
  import { serve as serve2 } from "@hono/node-server";
17088
+ import { fetchPublicHttpsText as fetchPublicHttpsText2 } from "@hyperframes/engine";
16991
17089
 
16992
17090
  // src/services/hyperframeLint.ts
16993
17091
  import {
@@ -17370,6 +17468,7 @@ function validateOutputResolutionOverride(body) {
17370
17468
  }
17371
17469
  return void 0;
17372
17470
  }
17471
+ var MAX_PREVIEW_HTML_BYTES = 20 * 1024 * 1024;
17373
17472
  function prepareProjectDirectory(projectDir, options) {
17374
17473
  const candidate = nonEmptyString(projectDir);
17375
17474
  if (!candidate) return null;
@@ -17390,11 +17489,7 @@ async function resolveInlineRenderHtml(body) {
17390
17489
  if (!previewUrl)
17391
17490
  return { error: "Missing render source: provide projectDir, previewUrl, or html" };
17392
17491
  try {
17393
- const response = await fetch(previewUrl, { method: "GET" });
17394
- if (!response.ok) {
17395
- return { error: `Failed to fetch previewUrl: ${response.status} ${response.statusText}` };
17396
- }
17397
- return { html: await response.text() };
17492
+ return { html: await fetchPublicHttpsText2(previewUrl, { maxBytes: MAX_PREVIEW_HTML_BYTES }) };
17398
17493
  } catch (error) {
17399
17494
  return {
17400
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.196",
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.196",
39
- "@hyperframes/parsers": "0.8.196",
40
- "@hyperframes/core": "0.8.196",
41
- "@hyperframes/lint": "0.8.196",
42
- "@hyperframes/studio-server": "0.8.196"
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.196"
51
+ "@hyperframes/aws-lambda": "0.8.198"
52
52
  },
53
53
  "engines": {
54
54
  "node": ">=22"