@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/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
@@ -7309,6 +7309,70 @@ async function measurePlanSecondsPerFrame(input) {
7309
7309
  }
7310
7310
  }
7311
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
+
7312
7376
  // src/services/render/stages/freezePlan.ts
7313
7377
  import { existsSync as existsSync13, mkdirSync as mkdirSync7, readFileSync as readFileSync8, readdirSync as readdirSync5, writeFileSync as writeFileSync4 } from "node:fs";
7314
7378
  import { join as join17, relative as relative3, resolve as resolve7 } from "node:path";
@@ -8424,6 +8488,7 @@ var PLAN_PROJECT_DIR_SKIP_SEGMENTS = /* @__PURE__ */ new Set([
8424
8488
  var DEFAULT_CHUNK_SIZE = 240;
8425
8489
  var DEFAULT_MAX_PARALLEL_CHUNKS = 16;
8426
8490
  var MIN_CHUNK_SIZE = 10;
8491
+ var MAX_CHUNK_SIZE = 3600;
8427
8492
  var MAX_PARALLEL_CHUNKS_CEILING = 256;
8428
8493
  var ESTIMATED_SECONDS_PER_MEGAPIXEL_FRAME = 0.11;
8429
8494
  var ESTIMATED_CHUNK_OVERHEAD_SECONDS = 60;
@@ -8450,6 +8515,11 @@ var ChunkExceedsWorkerTimeoutError = class extends Error {
8450
8515
  this.name = "ChunkExceedsWorkerTimeoutError";
8451
8516
  }
8452
8517
  };
8518
+ function framesWithinWorkerTimeout(workerTimeoutSeconds, secondsPerFrame) {
8519
+ return Math.floor(
8520
+ (workerTimeoutSeconds * WORKER_TIMEOUT_MARGIN - ESTIMATED_CHUNK_OVERHEAD_SECONDS) / secondsPerFrame
8521
+ );
8522
+ }
8453
8523
  function resolveMaxParallelChunks(config, totalFrames, megapixels, log = defaultLogger, measuredSecondsPerFrame) {
8454
8524
  if (config.maxParallelChunks !== void 0) return config.maxParallelChunks;
8455
8525
  const { workerTimeoutSeconds, workerConcurrency } = config;
@@ -8462,9 +8532,7 @@ function resolveMaxParallelChunks(config, totalFrames, megapixels, log = default
8462
8532
  const minFramesPerChunk = Math.ceil(ESTIMATED_CHUNK_OVERHEAD_SECONDS / secondsPerFrame);
8463
8533
  const wide = Math.max(1, Math.min(wave, Math.floor(totalFrames / minFramesPerChunk)));
8464
8534
  if (workerTimeoutSeconds === void 0) return wide;
8465
- const maxFramesPerChunk = Math.floor(
8466
- (workerTimeoutSeconds * WORKER_TIMEOUT_MARGIN - ESTIMATED_CHUNK_OVERHEAD_SECONDS) / secondsPerFrame
8467
- );
8535
+ const maxFramesPerChunk = framesWithinWorkerTimeout(workerTimeoutSeconds, secondsPerFrame);
8468
8536
  const needed = maxFramesPerChunk < 1 ? Infinity : Math.ceil(totalFrames / maxFramesPerChunk);
8469
8537
  if (needed > MAX_PARALLEL_CHUNKS_CEILING) {
8470
8538
  const framesAtCeiling = Math.ceil(totalFrames / MAX_PARALLEL_CHUNKS_CEILING);
@@ -8596,6 +8664,15 @@ function buildChunkSlices(totalFrames, chunkCount, effectiveChunkSize) {
8596
8664
  }
8597
8665
  return slices;
8598
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
+ }
8599
8676
  async function readFontSnapshotSha() {
8600
8677
  const module = await import("@hyperframes/core/fonts/embedded-data");
8601
8678
  const data = module.EMBEDDED_FONT_DATA;
@@ -9045,6 +9122,14 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
9045
9122
  const digests = options.digests ?? /* @__PURE__ */ new Map();
9046
9123
  const separateAudio = plansSeparateAudio(config, options);
9047
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
+ });
9048
9133
  stageStarted = Date.now();
9049
9134
  const [audioOutcome, overlapOutcome, frameCostOutcome] = await Promise.allSettled([
9050
9135
  separateAudio ? null : mixPlanAudio(c, finalCompiledDir).finally(() => logPlanStage(log, "audio", stageStarted)),
@@ -9095,7 +9180,16 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
9095
9180
  maxParallel,
9096
9181
  config.targetChunkFrames
9097
9182
  );
9098
- 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
+ );
9099
9193
  const audioResult = audioOutcome.value;
9100
9194
  if (audioResult?.audioError) {
9101
9195
  applyDistributedAudioWarningPolicy(job, audioResult.audioError, audioResult.audioFailures, log);
@@ -11147,7 +11241,6 @@ var ALLOWED_RUNTIME_CAPS = ["lambda", "temporal", "cloud-run-job", "k8s-job", "n
11147
11241
  var ALLOWED_HDR_MODES = ["auto", "force-sdr"];
11148
11242
  var MAX_DIMENSION = 7680;
11149
11243
  var MIN_DIMENSION = 16;
11150
- var MAX_CHUNK_SIZE = 3600;
11151
11244
  function validateDistributedRenderConfig(config) {
11152
11245
  if (config === null || typeof config !== "object") {
11153
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": "1010f54ffc72ff6dd0d697239903eaac0494d831922023edbfbbf234f86d0a1f",
4
+ "sha256": "34e66a8b6f8a0c9e7cb0895fc3e22c3a21b0a6240b0b47fb3fcf1621087b919b",
5
5
  "artifacts": {
6
6
  "iife": "hyperframe.runtime.iife.js",
7
7
  "esm": "hyperframe.runtime.mjs"