@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 +1 -1
- package/dist/distributed.js +98 -5
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +16 -16
- package/dist/index.js +101 -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
|
@@ -14571,6 +14571,70 @@ async function measurePlanSecondsPerFrame(input) {
|
|
|
14571
14571
|
}
|
|
14572
14572
|
}
|
|
14573
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
|
+
|
|
14574
14638
|
// src/services/render/stages/freezePlan.ts
|
|
14575
14639
|
import { existsSync as existsSync19, mkdirSync as mkdirSync10, readFileSync as readFileSync10, readdirSync as readdirSync7, writeFileSync as writeFileSync6 } from "node:fs";
|
|
14576
14640
|
import { join as join29, relative as relative3, resolve as resolve8 } from "node:path";
|
|
@@ -15685,6 +15749,7 @@ var PLAN_PROJECT_DIR_SKIP_SEGMENTS = /* @__PURE__ */ new Set([
|
|
|
15685
15749
|
]);
|
|
15686
15750
|
var DEFAULT_MAX_PARALLEL_CHUNKS = 16;
|
|
15687
15751
|
var MIN_CHUNK_SIZE = 10;
|
|
15752
|
+
var MAX_CHUNK_SIZE = 3600;
|
|
15688
15753
|
var MAX_PARALLEL_CHUNKS_CEILING = 256;
|
|
15689
15754
|
var ESTIMATED_SECONDS_PER_MEGAPIXEL_FRAME = 0.11;
|
|
15690
15755
|
var ESTIMATED_CHUNK_OVERHEAD_SECONDS = 60;
|
|
@@ -15711,6 +15776,11 @@ var ChunkExceedsWorkerTimeoutError = class extends Error {
|
|
|
15711
15776
|
this.name = "ChunkExceedsWorkerTimeoutError";
|
|
15712
15777
|
}
|
|
15713
15778
|
};
|
|
15779
|
+
function framesWithinWorkerTimeout(workerTimeoutSeconds, secondsPerFrame) {
|
|
15780
|
+
return Math.floor(
|
|
15781
|
+
(workerTimeoutSeconds * WORKER_TIMEOUT_MARGIN - ESTIMATED_CHUNK_OVERHEAD_SECONDS) / secondsPerFrame
|
|
15782
|
+
);
|
|
15783
|
+
}
|
|
15714
15784
|
function resolveMaxParallelChunks(config, totalFrames, megapixels, log = defaultLogger, measuredSecondsPerFrame) {
|
|
15715
15785
|
if (config.maxParallelChunks !== void 0) return config.maxParallelChunks;
|
|
15716
15786
|
const { workerTimeoutSeconds, workerConcurrency } = config;
|
|
@@ -15723,9 +15793,7 @@ function resolveMaxParallelChunks(config, totalFrames, megapixels, log = default
|
|
|
15723
15793
|
const minFramesPerChunk = Math.ceil(ESTIMATED_CHUNK_OVERHEAD_SECONDS / secondsPerFrame);
|
|
15724
15794
|
const wide = Math.max(1, Math.min(wave, Math.floor(totalFrames / minFramesPerChunk)));
|
|
15725
15795
|
if (workerTimeoutSeconds === void 0) return wide;
|
|
15726
|
-
const maxFramesPerChunk =
|
|
15727
|
-
(workerTimeoutSeconds * WORKER_TIMEOUT_MARGIN - ESTIMATED_CHUNK_OVERHEAD_SECONDS) / secondsPerFrame
|
|
15728
|
-
);
|
|
15796
|
+
const maxFramesPerChunk = framesWithinWorkerTimeout(workerTimeoutSeconds, secondsPerFrame);
|
|
15729
15797
|
const needed = maxFramesPerChunk < 1 ? Infinity : Math.ceil(totalFrames / maxFramesPerChunk);
|
|
15730
15798
|
if (needed > MAX_PARALLEL_CHUNKS_CEILING) {
|
|
15731
15799
|
const framesAtCeiling = Math.ceil(totalFrames / MAX_PARALLEL_CHUNKS_CEILING);
|
|
@@ -15854,6 +15922,15 @@ function buildChunkSlices(totalFrames, chunkCount, effectiveChunkSize) {
|
|
|
15854
15922
|
}
|
|
15855
15923
|
return slices;
|
|
15856
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
|
+
}
|
|
15857
15934
|
async function readFontSnapshotSha() {
|
|
15858
15935
|
const module = await import("@hyperframes/core/fonts/embedded-data");
|
|
15859
15936
|
const data = module.EMBEDDED_FONT_DATA;
|
|
@@ -16303,6 +16380,14 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
16303
16380
|
const digests = options.digests ?? /* @__PURE__ */ new Map();
|
|
16304
16381
|
const separateAudio = plansSeparateAudio(config, options);
|
|
16305
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
|
+
});
|
|
16306
16391
|
stageStarted = Date.now();
|
|
16307
16392
|
const [audioOutcome, overlapOutcome, frameCostOutcome] = await Promise.allSettled([
|
|
16308
16393
|
separateAudio ? null : mixPlanAudio(c, finalCompiledDir).finally(() => logPlanStage(log, "audio", stageStarted)),
|
|
@@ -16353,7 +16438,16 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
16353
16438
|
maxParallel,
|
|
16354
16439
|
config.targetChunkFrames
|
|
16355
16440
|
);
|
|
16356
|
-
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
|
+
);
|
|
16357
16451
|
const audioResult = audioOutcome.value;
|
|
16358
16452
|
if (audioResult?.audioError) {
|
|
16359
16453
|
applyDistributedAudioWarningPolicy(job, audioResult.audioError, audioResult.audioFailures, log);
|
|
@@ -16499,7 +16593,6 @@ var ALLOWED_RUNTIME_CAPS = ["lambda", "temporal", "cloud-run-job", "k8s-job", "n
|
|
|
16499
16593
|
var ALLOWED_HDR_MODES = ["auto", "force-sdr"];
|
|
16500
16594
|
var MAX_DIMENSION = 7680;
|
|
16501
16595
|
var MIN_DIMENSION = 16;
|
|
16502
|
-
var MAX_CHUNK_SIZE = 3600;
|
|
16503
16596
|
function validateDistributedRenderConfig(config) {
|
|
16504
16597
|
if (config === null || typeof config !== "object") {
|
|
16505
16598
|
throw new InvalidConfigError("config", "must be an object");
|
|
@@ -16992,6 +17085,7 @@ import crypto from "node:crypto";
|
|
|
16992
17085
|
import { Hono as Hono2 } from "hono";
|
|
16993
17086
|
import { streamSSE } from "hono/streaming";
|
|
16994
17087
|
import { serve as serve2 } from "@hono/node-server";
|
|
17088
|
+
import { fetchPublicHttpsText as fetchPublicHttpsText2 } from "@hyperframes/engine";
|
|
16995
17089
|
|
|
16996
17090
|
// src/services/hyperframeLint.ts
|
|
16997
17091
|
import {
|
|
@@ -17374,6 +17468,7 @@ function validateOutputResolutionOverride(body) {
|
|
|
17374
17468
|
}
|
|
17375
17469
|
return void 0;
|
|
17376
17470
|
}
|
|
17471
|
+
var MAX_PREVIEW_HTML_BYTES = 20 * 1024 * 1024;
|
|
17377
17472
|
function prepareProjectDirectory(projectDir, options) {
|
|
17378
17473
|
const candidate = nonEmptyString(projectDir);
|
|
17379
17474
|
if (!candidate) return null;
|
|
@@ -17394,11 +17489,7 @@ async function resolveInlineRenderHtml(body) {
|
|
|
17394
17489
|
if (!previewUrl)
|
|
17395
17490
|
return { error: "Missing render source: provide projectDir, previewUrl, or html" };
|
|
17396
17491
|
try {
|
|
17397
|
-
|
|
17398
|
-
if (!response.ok) {
|
|
17399
|
-
return { error: `Failed to fetch previewUrl: ${response.status} ${response.statusText}` };
|
|
17400
|
-
}
|
|
17401
|
-
return { html: await response.text() };
|
|
17492
|
+
return { html: await fetchPublicHttpsText2(previewUrl, { maxBytes: MAX_PREVIEW_HTML_BYTES }) };
|
|
17402
17493
|
} catch (error) {
|
|
17403
17494
|
return {
|
|
17404
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/parsers": "0.8.
|
|
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"
|