@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/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 =
|
|
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 =
|
|
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
|
-
|
|
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)}`
|
package/dist/public-server.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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/
|
|
39
|
-
"@hyperframes/
|
|
40
|
-
"@hyperframes/
|
|
41
|
-
"@hyperframes/
|
|
42
|
-
"@hyperframes/studio-server": "0.8.
|
|
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.
|
|
51
|
+
"@hyperframes/aws-lambda": "0.8.198"
|
|
52
52
|
},
|
|
53
53
|
"engines": {
|
|
54
54
|
"node": ">=22"
|