@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 +1 -1
- package/dist/distributed.js +102 -5
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +58 -58
- package/dist/index.js +105 -10
- package/dist/public-server.js +6 -5
- package/dist/services/distributed/chunkWeights.d.ts +16 -0
- package/dist/services/distributed/plan.d.ts +7 -0
- package/package.json +7 -7
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
|
package/dist/distributed.js
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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": "
|
|
4
|
+
"sha256": "34e66a8b6f8a0c9e7cb0895fc3e22c3a21b0a6240b0b47fb3fcf1621087b919b",
|
|
5
5
|
"artifacts": {
|
|
6
6
|
"iife": "hyperframe.runtime.iife.js",
|
|
7
7
|
"esm": "hyperframe.runtime.mjs"
|