@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/README.md CHANGED
@@ -35,7 +35,7 @@ The producer can also run as a render server, accepting render requests over HTT
35
35
  import { startServer } from "@hyperframes/producer";
36
36
 
37
37
  await startServer({ port: 8080 });
38
- // POST /render or /render/stream with projectDir, html, or previewUrl and render options
38
+ // POST /render or /render/stream with projectDir, html, or previewUrl (public HTTPS only) and render options
39
39
  ```
40
40
 
41
41
  Both HTTP routes accept optional `pageMotionBlur: "off"` to disable authored page
@@ -6909,6 +6909,10 @@ function readVideoMetadata(value, field) {
6909
6909
  hasAudio: readBoolean(record.hasAudio, `${field}.hasAudio`),
6910
6910
  isVFR: readBoolean(record.isVFR, `${field}.isVFR`),
6911
6911
  hasAlpha: readBoolean(record.hasAlpha, `${field}.hasAlpha`),
6912
+ // Optional: plans from producers without the decode probe keep today's png for vp8/vp9.
6913
+ ...record.alphaPlaneDecoded === void 0 ? {} : {
6914
+ alphaPlaneDecoded: readBoolean(record.alphaPlaneDecoded, `${field}.alphaPlaneDecoded`)
6915
+ },
6912
6916
  colorSpace
6913
6917
  };
6914
6918
  }
@@ -7305,6 +7309,70 @@ async function measurePlanSecondsPerFrame(input) {
7305
7309
  }
7306
7310
  }
7307
7311
 
7312
+ // src/services/distributed/chunkWeights.ts
7313
+ import { resolveFrameFormat } from "@hyperframes/engine";
7314
+ var PAGE_BASE_WEIGHT = 0.7;
7315
+ var PNG_WEIGHT_FACTOR = 1.5;
7316
+ function isUniformFrameCost(weights) {
7317
+ let lightest = Infinity;
7318
+ let heaviest = 0;
7319
+ for (const weight of weights) {
7320
+ lightest = Math.min(lightest, weight);
7321
+ heaviest = Math.max(heaviest, weight);
7322
+ }
7323
+ return heaviest <= lightest * 1.05;
7324
+ }
7325
+ function planFrameWeights(input) {
7326
+ const { totalFrames, fps } = input;
7327
+ const delta = new Float64Array(totalFrames + 1);
7328
+ const outputPixels = input.width * input.height;
7329
+ const extractedById = new Map(input.extracted.map((entry) => [entry.videoId, entry]));
7330
+ for (const video of input.videos) {
7331
+ const entry = extractedById.get(video.id);
7332
+ if (!entry || video.hidden) continue;
7333
+ const start = Math.max(0, Math.round(video.start * fps));
7334
+ const end = Math.min(totalFrames, Math.round(video.end * fps));
7335
+ if (!(end > start)) continue;
7336
+ const { width, height } = entry.metadata;
7337
+ const factor = resolveFrameFormat(entry.metadata) === "png" ? PNG_WEIGHT_FACTOR : 1;
7338
+ const weight = Math.min(width * height, outputPixels) / 1e6 * factor;
7339
+ delta[start] = delta[start] + weight;
7340
+ delta[end] = delta[end] - weight;
7341
+ }
7342
+ const weights = new Float64Array(totalFrames);
7343
+ let active = 0;
7344
+ for (let frame = 0; frame < totalFrames; frame++) {
7345
+ active += delta[frame];
7346
+ weights[frame] = PAGE_BASE_WEIGHT + active;
7347
+ }
7348
+ return weights;
7349
+ }
7350
+ function buildWeightedChunkSlices(weights, chunkCount, minFrames, maxFrames) {
7351
+ if (isUniformFrameCost(weights)) return null;
7352
+ const totalFrames = weights.length;
7353
+ const total = weights.reduce((sum2, weight) => sum2 + weight, 0);
7354
+ const minSize = Math.max(1, Math.min(minFrames, Math.floor(totalFrames / chunkCount)));
7355
+ const maxSize = Math.max(maxFrames, Math.ceil(totalFrames / chunkCount));
7356
+ const slices = [];
7357
+ let startFrame = 0;
7358
+ let frame = 0;
7359
+ let costBefore = 0;
7360
+ for (let index = 0; index < chunkCount - 1; index++) {
7361
+ const target = (index + 1) * total / chunkCount;
7362
+ while (frame < totalFrames && costBefore + weights[frame] / 2 < target) {
7363
+ costBefore += weights[frame++];
7364
+ }
7365
+ const remaining = chunkCount - index - 1;
7366
+ const low = Math.max(startFrame + minSize, totalFrames - remaining * maxSize);
7367
+ const high = Math.min(startFrame + maxSize, totalFrames - remaining * minSize);
7368
+ const endFrame = Math.min(high, Math.max(low, frame));
7369
+ slices.push({ index, startFrame, endFrame });
7370
+ startFrame = endFrame;
7371
+ }
7372
+ slices.push({ index: chunkCount - 1, startFrame, endFrame: totalFrames });
7373
+ return slices;
7374
+ }
7375
+
7308
7376
  // src/services/render/stages/freezePlan.ts
7309
7377
  import { existsSync as existsSync13, mkdirSync as mkdirSync7, readFileSync as readFileSync8, readdirSync as readdirSync5, writeFileSync as writeFileSync4 } from "node:fs";
7310
7378
  import { join as join17, relative as relative3, resolve as resolve7 } from "node:path";
@@ -8420,6 +8488,7 @@ var PLAN_PROJECT_DIR_SKIP_SEGMENTS = /* @__PURE__ */ new Set([
8420
8488
  var DEFAULT_CHUNK_SIZE = 240;
8421
8489
  var DEFAULT_MAX_PARALLEL_CHUNKS = 16;
8422
8490
  var MIN_CHUNK_SIZE = 10;
8491
+ var MAX_CHUNK_SIZE = 3600;
8423
8492
  var MAX_PARALLEL_CHUNKS_CEILING = 256;
8424
8493
  var ESTIMATED_SECONDS_PER_MEGAPIXEL_FRAME = 0.11;
8425
8494
  var ESTIMATED_CHUNK_OVERHEAD_SECONDS = 60;
@@ -8446,6 +8515,11 @@ var ChunkExceedsWorkerTimeoutError = class extends Error {
8446
8515
  this.name = "ChunkExceedsWorkerTimeoutError";
8447
8516
  }
8448
8517
  };
8518
+ function framesWithinWorkerTimeout(workerTimeoutSeconds, secondsPerFrame) {
8519
+ return Math.floor(
8520
+ (workerTimeoutSeconds * WORKER_TIMEOUT_MARGIN - ESTIMATED_CHUNK_OVERHEAD_SECONDS) / secondsPerFrame
8521
+ );
8522
+ }
8449
8523
  function resolveMaxParallelChunks(config, totalFrames, megapixels, log = defaultLogger, measuredSecondsPerFrame) {
8450
8524
  if (config.maxParallelChunks !== void 0) return config.maxParallelChunks;
8451
8525
  const { workerTimeoutSeconds, workerConcurrency } = config;
@@ -8458,9 +8532,7 @@ function resolveMaxParallelChunks(config, totalFrames, megapixels, log = default
8458
8532
  const minFramesPerChunk = Math.ceil(ESTIMATED_CHUNK_OVERHEAD_SECONDS / secondsPerFrame);
8459
8533
  const wide = Math.max(1, Math.min(wave, Math.floor(totalFrames / minFramesPerChunk)));
8460
8534
  if (workerTimeoutSeconds === void 0) return wide;
8461
- const maxFramesPerChunk = Math.floor(
8462
- (workerTimeoutSeconds * WORKER_TIMEOUT_MARGIN - ESTIMATED_CHUNK_OVERHEAD_SECONDS) / secondsPerFrame
8463
- );
8535
+ const maxFramesPerChunk = framesWithinWorkerTimeout(workerTimeoutSeconds, secondsPerFrame);
8464
8536
  const needed = maxFramesPerChunk < 1 ? Infinity : Math.ceil(totalFrames / maxFramesPerChunk);
8465
8537
  if (needed > MAX_PARALLEL_CHUNKS_CEILING) {
8466
8538
  const framesAtCeiling = Math.ceil(totalFrames / MAX_PARALLEL_CHUNKS_CEILING);
@@ -8592,6 +8664,15 @@ function buildChunkSlices(totalFrames, chunkCount, effectiveChunkSize) {
8592
8664
  }
8593
8665
  return slices;
8594
8666
  }
8667
+ function buildPlanChunkSlices(frameWeights, config, chunkCount, effectiveChunkSize, maxFramesPerChunk = Infinity) {
8668
+ const weighted = config.chunkSize === void 0 ? buildWeightedChunkSlices(
8669
+ frameWeights,
8670
+ chunkCount,
8671
+ MIN_CHUNK_SIZE,
8672
+ Math.min(config.targetChunkFrames ?? MAX_CHUNK_SIZE, MAX_CHUNK_SIZE, maxFramesPerChunk)
8673
+ ) : null;
8674
+ return weighted ?? buildChunkSlices(frameWeights.length, chunkCount, effectiveChunkSize);
8675
+ }
8595
8676
  async function readFontSnapshotSha() {
8596
8677
  const module = await import("@hyperframes/core/fonts/embedded-data");
8597
8678
  const data = module.EMBEDDED_FONT_DATA;
@@ -9041,6 +9122,14 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
9041
9122
  const digests = options.digests ?? /* @__PURE__ */ new Map();
9042
9123
  const separateAudio = plansSeparateAudio(config, options);
9043
9124
  const chunksCarryAudio = !separateAudio && carriesChunkAudio(config, options);
9125
+ const frameWeights = planFrameWeights({
9126
+ videos: planVideosJson.videos,
9127
+ extracted: planVideosJson.extracted,
9128
+ totalFrames,
9129
+ fps: config.fps,
9130
+ width,
9131
+ height
9132
+ });
9044
9133
  stageStarted = Date.now();
9045
9134
  const [audioOutcome, overlapOutcome, frameCostOutcome] = await Promise.allSettled([
9046
9135
  separateAudio ? null : mixPlanAudio(c, finalCompiledDir).finally(() => logPlanStage(log, "audio", stageStarted)),
@@ -9091,7 +9180,16 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
9091
9180
  maxParallel,
9092
9181
  config.targetChunkFrames
9093
9182
  );
9094
- const chunks = buildChunkSlices(totalFrames, chunkCount, effectiveChunkSize);
9183
+ const chunks = buildPlanChunkSlices(
9184
+ frameWeights,
9185
+ config,
9186
+ chunkCount,
9187
+ effectiveChunkSize,
9188
+ chunkSizingUsesFrameCost(config) && config.workerTimeoutSeconds !== void 0 ? framesWithinWorkerTimeout(
9189
+ config.workerTimeoutSeconds,
9190
+ estimateFrameSeconds(megapixels, frameCostOutcome.value)
9191
+ ) : void 0
9192
+ );
9095
9193
  const audioResult = audioOutcome.value;
9096
9194
  if (audioResult?.audioError) {
9097
9195
  applyDistributedAudioWarningPolicy(job, audioResult.audioError, audioResult.audioFailures, log);
@@ -11143,7 +11241,6 @@ var ALLOWED_RUNTIME_CAPS = ["lambda", "temporal", "cloud-run-job", "k8s-job", "n
11143
11241
  var ALLOWED_HDR_MODES = ["auto", "force-sdr"];
11144
11242
  var MAX_DIMENSION = 7680;
11145
11243
  var MIN_DIMENSION = 16;
11146
- var MAX_CHUNK_SIZE = 3600;
11147
11244
  function validateDistributedRenderConfig(config) {
11148
11245
  if (config === null || typeof config !== "object") {
11149
11246
  throw new InvalidConfigError("config", "must be an object");
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": "0.1.0",
3
3
  "buildId": "dev",
4
- "sha256": "76ecb9a94777082e1cb85f468f089ade395649a08149ad8a6795fa3ff6062cc8",
4
+ "sha256": "34e66a8b6f8a0c9e7cb0895fc3e22c3a21b0a6240b0b47fb3fcf1621087b919b",
5
5
  "artifacts": {
6
6
  "iife": "hyperframe.runtime.iife.js",
7
7
  "esm": "hyperframe.runtime.mjs"