@hyperframes/producer 0.9.12 → 0.9.14

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/dist/index.js CHANGED
@@ -1223,27 +1223,25 @@ function createMemorySampler(intervalMs = 250) {
1223
1223
  };
1224
1224
  }
1225
1225
  var warnedSymlinkFallback = false;
1226
+ var copyCannotFix = (error) => ["ENOSPC", "EDQUOT", "EEXIST", "ENOENT"].includes(
1227
+ error?.code ?? ""
1228
+ );
1226
1229
  function tryWindowsFrameJunction(fileSystem, src, dest) {
1227
1230
  if (process.platform !== "win32") return false;
1228
1231
  try {
1229
1232
  fileSystem.symlinkSync(src, dest, "junction");
1230
1233
  return true;
1231
1234
  } catch (error) {
1232
- const code = error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : void 0;
1233
- if (["EPERM", "EACCES", "UNKNOWN", "EINVAL", "ENOSYS", "EOPNOTSUPP", "ENOTSUP"].includes(
1234
- code ?? ""
1235
- )) {
1236
- return false;
1237
- }
1238
- throw error;
1235
+ if (copyCannotFix(error)) throw error;
1236
+ return false;
1239
1237
  }
1240
1238
  }
1241
1239
  function linkOrCopyFrameDir(fileSystem, src, dest) {
1242
1240
  try {
1243
1241
  fileSystem.symlinkSync(src, dest, "dir");
1244
1242
  } catch (err) {
1243
+ if (copyCannotFix(err)) throw err;
1245
1244
  const code = err?.code;
1246
- if (code !== "EPERM" && code !== "EACCES" && code !== "UNKNOWN") throw err;
1247
1245
  if (tryWindowsFrameJunction(fileSystem, src, dest)) return;
1248
1246
  if (!warnedSymlinkFallback) {
1249
1247
  warnedSymlinkFallback = true;
@@ -3743,6 +3741,7 @@ function createStallClock(now = () => Date.now()) {
3743
3741
 
3744
3742
  // src/services/render/stages/captureStreamingStage.ts
3745
3743
  var DEFAULT_CAPTURE_STALL_MS = 6e4;
3744
+ var SESSION_INIT_STALL_MS = 3e5;
3746
3745
  var DE_STALL_POLL_MS = 5e3;
3747
3746
  var STALL_ADVICE = "Chrome stopped answering. If another app is keeping the GPU or CPU busy (a 3D render, a game, another export), let it finish or render one job at a time, then retry.";
3748
3747
  function poolFailure(error, probeSession) {
@@ -3773,6 +3772,14 @@ function captureModeLabel(mode) {
3773
3772
  if (mode === "beginframe") return "BeginFrame";
3774
3773
  return "screenshot";
3775
3774
  }
3775
+ var OPENING_CAPTURE_SESSION = {
3776
+ captureMode: "screenshot",
3777
+ captureStep: "opening the capture session"
3778
+ };
3779
+ var OPENING_RISK_SESSION = {
3780
+ captureMode: "screenshot",
3781
+ captureStep: "opening the screenshot session for effect frames"
3782
+ };
3776
3783
  var SequentialCaptureStallError = class extends Error {
3777
3784
  captureMode;
3778
3785
  constructor(session, stallTimeoutMs, frameIndex, totalFrames) {
@@ -3922,7 +3929,12 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
3922
3929
  const guard = createDrainFrameGuard({ log, stats, frameTime });
3923
3930
  const guardFrame = (item, buf) => item.trusted ? Promise.resolve(buf) : guard(session, item.idx, buf);
3924
3931
  const captureRisk = async (idx) => {
3925
- await risk.open();
3932
+ await raceAgainstStall(
3933
+ risk.open(),
3934
+ stallTimeoutMs - quiet.idle(0),
3935
+ { session: OPENING_RISK_SESSION, frameIndex: idx, totalFrames, stallTimeoutMs },
3936
+ abortSignal
3937
+ );
3926
3938
  quiet.reset();
3927
3939
  return raceAgainstStall(
3928
3940
  risk.capture(idx, frameTime(idx)),
@@ -4281,19 +4293,38 @@ async function runCaptureStreamingStage(input) {
4281
4293
  let resumes = 0;
4282
4294
  for (; ; ) {
4283
4295
  const videoInjector = createRenderVideoFrameInjector();
4284
- const session = probeSession ?? await createCaptureSession(
4285
- fileServer.url,
4286
- framesDir,
4287
- buildCaptureOptions(),
4288
- videoInjector,
4289
- captureCfg
4296
+ const initStallMs = Math.max(SESSION_INIT_STALL_MS, resolveCaptureStallTimeoutMs());
4297
+ const setup = createStallClock();
4298
+ const withinInitBudget = (work, stalled) => raceAgainstStall(
4299
+ work,
4300
+ initStallMs - setup.idle(0),
4301
+ { session: stalled, frameIndex: resume.from, totalFrames, stallTimeoutMs: initStallMs },
4302
+ abortSignal
4290
4303
  );
4304
+ const openSession = async () => {
4305
+ const opening = createCaptureSession(
4306
+ fileServer.url,
4307
+ framesDir,
4308
+ buildCaptureOptions(),
4309
+ videoInjector,
4310
+ captureCfg
4311
+ );
4312
+ try {
4313
+ return await withinInitBudget(opening, OPENING_CAPTURE_SESSION);
4314
+ } catch (error) {
4315
+ void opening.then(closeCaptureSession, () => {
4316
+ });
4317
+ throw error;
4318
+ }
4319
+ };
4320
+ const session = probeSession ?? await openSession();
4291
4321
  captureBeyondViewport = session.options.captureBeyondViewport;
4292
4322
  if (probeSession) {
4293
4323
  prepareCaptureSessionForReuse(session, framesDir, videoInjector);
4294
4324
  probeSession = null;
4295
4325
  }
4296
4326
  let riskSession = null;
4327
+ let attemptClosed = false;
4297
4328
  const openRiskSession = async () => {
4298
4329
  const opened = await createCaptureSession(
4299
4330
  fileServer.url,
@@ -4307,6 +4338,10 @@ async function runCaptureStreamingStage(input) {
4307
4338
  enableBrowserPool: false
4308
4339
  }
4309
4340
  );
4341
+ if (attemptClosed) {
4342
+ await closeCaptureSession(opened);
4343
+ throw new Error("[Render] Capture attempt ended while its screenshot session opened");
4344
+ }
4310
4345
  riskSession = opened;
4311
4346
  await initializeSession(opened);
4312
4347
  return opened;
@@ -4314,9 +4349,9 @@ async function runCaptureStreamingStage(input) {
4314
4349
  let resumed = false;
4315
4350
  try {
4316
4351
  if (!session.isInitialized) {
4317
- await initializeSession(session);
4352
+ await withinInitBudget(initializeSession(session), session);
4318
4353
  }
4319
- await completeDeferredDrawElementInit(session);
4354
+ await withinInitBudget(completeDeferredDrawElementInit(session), session);
4320
4355
  assertNotAborted();
4321
4356
  lastBrowserConsole = session.browserConsoleBuffer;
4322
4357
  if (session.workerEncodeEnabled) {
@@ -4399,6 +4434,7 @@ async function runCaptureStreamingStage(input) {
4399
4434
  resumes++;
4400
4435
  resumed = true;
4401
4436
  } finally {
4437
+ attemptClosed = true;
4402
4438
  lastBrowserConsole = session.browserConsoleBuffer;
4403
4439
  await closeCaptureSession(session);
4404
4440
  if (riskSession) await closeCaptureSession(riskSession);
@@ -5865,7 +5901,7 @@ import { suggestMatchingPreset } from "@hyperframes/core";
5865
5901
  // src/services/htmlCompiler.ts
5866
5902
  import { decodedUrlPath } from "@hyperframes/parsers/asset-paths";
5867
5903
  import { createHash as createHash6 } from "node:crypto";
5868
- import { createReadStream as createReadStream2, existsSync as existsSync10, mkdirSync as mkdirSync7 } from "fs";
5904
+ import { createReadStream as createReadStream2, existsSync as existsSync10, mkdirSync as mkdirSync7, readFileSync as readFileSync7 } from "fs";
5869
5905
  import { join as join15, dirname as dirname7, resolve as resolve7, basename as basename4, relative as relative2 } from "path";
5870
5906
  import { parseHTML as parseHTML5 } from "linkedom";
5871
5907
  import {
@@ -7840,6 +7876,25 @@ ${fontFaceBlocks.join("\n")}
7840
7876
  }
7841
7877
  return result;
7842
7878
  }
7879
+ function projectStylesheetFontFamilies(html, projectDir) {
7880
+ const families = [];
7881
+ for (const [tag] of html.matchAll(/<link\b[^>]*\brel=["']stylesheet["'][^>]*>/gi)) {
7882
+ const href = /\bhref=["']([^"']+)["']/i.exec(tag)?.[1];
7883
+ if (!href || /^(?:[a-z][a-z\d+.-]*:|\/\/)/i.test(href)) continue;
7884
+ const sheet = resolve7(projectDir, decodedUrlPath(href).replace(/^(?:\.?\/)+/, ""));
7885
+ if (!isPathInside2(sheet, projectDir) || !existsSync10(sheet)) continue;
7886
+ for (const face of extractFontFaceBlocks(readFileSync7(sheet, "utf8"))) {
7887
+ const family = /font-family\s*:\s*([^;}]+)/i.exec(face)?.[1]?.trim().replace(/^['"]|['"]$/g, "");
7888
+ const shipsFile = [...face.matchAll(LOCAL_FONTFACE_URL_RE)].some(([, url = ""]) => {
7889
+ const path = decodedUrlPath(url);
7890
+ const file = path.startsWith("/") ? resolve7(projectDir, `.${path}`) : resolve7(dirname7(sheet), path);
7891
+ return isPathInside2(file, projectDir) && existsSync10(file);
7892
+ });
7893
+ if (family && shipsFile) families.push(family);
7894
+ }
7895
+ }
7896
+ return families;
7897
+ }
7843
7898
  function collectFontFaceUrls(html) {
7844
7899
  const styleBlockRe = /<style\b[^>]*>([\s\S]*?)<\/style>/gi;
7845
7900
  const urlSet = /* @__PURE__ */ new Set();
@@ -7871,11 +7926,12 @@ function collectFontFaceUrls(html) {
7871
7926
  return urlSet;
7872
7927
  }
7873
7928
  async function localizeRemoteFontFaces(html, downloadDir) {
7874
- const processed = await inlineExternalFontStylesheets(html);
7875
- const urlSet = collectFontFaceUrls(processed);
7929
+ return localizeFontFaceUrls(await inlineExternalFontStylesheets(html), downloadDir);
7930
+ }
7931
+ function localizeFontFaceUrls(html, downloadDir) {
7876
7932
  return downloadAndRewriteUrls(
7877
- urlSet,
7878
- processed,
7933
+ collectFontFaceUrls(html),
7934
+ html,
7879
7935
  join15(downloadDir, REMOTE_MEDIA_SUBDIR),
7880
7936
  "Remote font download failed",
7881
7937
  "Localized remote font face(s)",
@@ -8043,15 +8099,17 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
8043
8099
  const hasShaderTransitions = detectShaderTransitionUsage(sanitizedHtml);
8044
8100
  const usesThreeDTransforms = detectThreeDTransformUsage(sanitizedHtml);
8045
8101
  const hasAncestorBackgroundImage = detectAncestorBackgroundImage(sanitizedHtml);
8102
+ const fontSheetHtml = await inlineExternalFontStylesheets(
8103
+ promoteCssImportsToLinkTags(sanitizedHtml)
8104
+ );
8046
8105
  const normalizedFontHtml = normalizeSystemFontPrimaryFamilies(
8047
- injectTextRenderingRule(
8048
- coalesceHeadStylesAndBodyScripts(promoteCssImportsToLinkTags(sanitizedHtml))
8049
- )
8106
+ injectTextRenderingRule(coalesceHeadStylesAndBodyScripts(fontSheetHtml))
8050
8107
  );
8051
8108
  const coalescedHtml = await injectDeterministicFontFaces(normalizedFontHtml, {
8052
8109
  logger: defaultLogger,
8053
8110
  failClosedFontFetch: options.failClosedFontFetch === true,
8054
8111
  allowSystemFontCapture: options.allowSystemFontCapture,
8112
+ projectFontFamilies: projectStylesheetFontFamilies(fontSheetHtml, projectDir),
8055
8113
  abortSignal: options.abortSignal
8056
8114
  });
8057
8115
  const assembledHtml = await inlineExternalScripts(coalescedHtml);
@@ -8077,7 +8135,7 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
8077
8135
  );
8078
8136
  const { html: htmlWithLocalImages, remoteMediaAssets: remoteImageAssets } = await localizeRemoteImageSources(htmlWithLocalMedia, downloadDir);
8079
8137
  const { html: htmlWithLocalBg, remoteMediaAssets: remoteBgAssets } = await localizeRemoteBackgroundImages(htmlWithLocalImages, downloadDir);
8080
- const { html: htmlWithLocalizedFonts, remoteMediaAssets: remoteFontAssets } = await localizeRemoteFontFaces(htmlWithLocalBg, downloadDir);
8138
+ const { html: htmlWithLocalizedFonts, remoteMediaAssets: remoteFontAssets } = await localizeFontFaceUrls(htmlWithLocalBg, downloadDir);
8081
8139
  const {
8082
8140
  html: htmlWithJpegs,
8083
8141
  preparedAssets: preparedHeifAssets,
@@ -9528,17 +9586,24 @@ var VideoExtractionStageError = class extends Error {
9528
9586
  publicMetadata;
9529
9587
  };
9530
9588
  function assertVideoExtractionSucceeded(result) {
9531
- throwIfEncoderInterrupted(result);
9589
+ throwIfHostFailure(result);
9532
9590
  const error = buildVideoExtractionStageError(result);
9533
9591
  if (error) throw error;
9534
9592
  }
9535
- function throwIfEncoderInterrupted(result) {
9593
+ function throwIfHostFailure(result) {
9536
9594
  const interrupted = result.errors.find((failure) => failure.kind === "external_interruption");
9537
- if (!interrupted) return;
9538
- throw encoderFailureError("Video frame extraction failed", {
9539
- error: String(interrupted.error),
9540
- failureReason: "external_interruption"
9541
- });
9595
+ if (interrupted) {
9596
+ throw encoderFailureError("Video frame extraction failed", {
9597
+ error: String(interrupted.error),
9598
+ failureReason: "external_interruption"
9599
+ });
9600
+ }
9601
+ const full = result.errors.filter((failure) => failure.kind === "disk_full").length;
9602
+ if (full === 0) return;
9603
+ throw Object.assign(
9604
+ new Error(`ENOSPC: no space left on device, extracting the frames of ${full} video source(s)`),
9605
+ { code: "ENOSPC" }
9606
+ );
9542
9607
  }
9543
9608
  function buildVideoExtractionStageError(result) {
9544
9609
  if (result.success && result.errors.length === 0) return null;
@@ -9753,7 +9818,7 @@ async function runExtractVideosStage(input) {
9753
9818
  );
9754
9819
  extractionResult.phaseBreakdown.transientRetries = (extractionResult.phaseBreakdown.transientRetries ?? 0) + hdrProbeTransientRetries;
9755
9820
  assertNotAborted();
9756
- throwIfEncoderInterrupted(extractionResult);
9821
+ throwIfHostFailure(extractionResult);
9757
9822
  failureToEnforce = applyVideoExtractionFailurePolicy(extractionResult, extractionPolicy, log);
9758
9823
  materializeExtractedFramesForCompiledDir(extractionResult.extracted, compiledDir, {
9759
9824
  materializeSymlinks
@@ -11945,7 +12010,7 @@ import {
11945
12010
  existsSync as existsSync17,
11946
12011
  mkdirSync as mkdirSync9,
11947
12012
  readdirSync as readdirSync4,
11948
- readFileSync as readFileSync7,
12013
+ readFileSync as readFileSync8,
11949
12014
  rmSync as rmSync9,
11950
12015
  statSync as statSync7,
11951
12016
  writeFileSync as writeFileSync5
@@ -12039,7 +12104,7 @@ async function reencodeWithClearedWholeFrames(argsInput, run, startTime) {
12039
12104
  const error = formatFfmpegError(result.exitCode, result.stderr);
12040
12105
  return gifEncodeFailure(startTime, outputPath, error, result.failureReason);
12041
12106
  }
12042
- const gif = readFileSync7(outputPath);
12107
+ const gif = readFileSync8(outputPath);
12043
12108
  if (!clearGifFramesBeforeNext(gif)) {
12044
12109
  const error = "[FFmpeg] GIF output could not be parsed to clear its frames";
12045
12110
  return gifEncodeFailure(startTime, outputPath, error);
@@ -12072,7 +12137,7 @@ async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
12072
12137
  return gifEncodeFailure(startTime, outputPath, error, result.failureReason);
12073
12138
  }
12074
12139
  }
12075
- if (input.preserveAlpha && gifClearsAfterLeavingFrameInPlace(readFileSync7(outputPath)) !== false) {
12140
+ if (input.preserveAlpha && gifClearsAfterLeavingFrameInPlace(readFileSync8(outputPath)) !== false) {
12076
12141
  const failure = await reencodeWithClearedWholeFrames(argsInput, run, startTime);
12077
12142
  if (failure) return failure;
12078
12143
  }
@@ -12891,6 +12956,22 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
12891
12956
  }
12892
12957
  const remainingCount = countFrameRanges(remaining);
12893
12958
  const madeProgress = captureAttemptMadeProgress(frameCount, remainingCount);
12959
+ if (options.allowRetry && !madeProgress && (currentWorkers > 1 || initializationRetriesUsed === 0 && failure.kind === "protocol_timeout") && /\bNetwork\.enable timed out|Navigation timeout of \d+ ms exceeded/i.test(failure.message)) {
12960
+ initializationRetriesUsed++;
12961
+ const nextWorkers2 = getNextRetryWorkerCount(currentWorkers);
12962
+ options.log.warn(
12963
+ "[Render] Browser initialization timed out; retrying with fewer workers.",
12964
+ {
12965
+ fromWorkers: currentWorkers,
12966
+ toWorkers: nextWorkers2,
12967
+ error: failure.message
12968
+ }
12969
+ );
12970
+ currentWorkers = nextWorkers2;
12971
+ missingRanges = remaining;
12972
+ attempt++;
12973
+ continue;
12974
+ }
12894
12975
  if (options.allowRetry && isTransientCaptureRetryEligible(failure.kind, remaining, transientRetriesUsed)) {
12895
12976
  transientRetriesUsed++;
12896
12977
  options.log.warn(
@@ -12908,22 +12989,6 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
12908
12989
  pendingTransientRetry = true;
12909
12990
  continue;
12910
12991
  }
12911
- if (options.allowRetry && !madeProgress && initializationRetriesUsed === 0 && failure.kind === "protocol_timeout" && /\bNetwork\.enable timed out/i.test(failure.message)) {
12912
- initializationRetriesUsed++;
12913
- const nextWorkers2 = getNextRetryWorkerCount(currentWorkers);
12914
- options.log.warn(
12915
- "[Render] Browser initialization timed out; retrying once with fresh sessions.",
12916
- {
12917
- fromWorkers: currentWorkers,
12918
- toWorkers: nextWorkers2,
12919
- error: failure.message
12920
- }
12921
- );
12922
- currentWorkers = nextWorkers2;
12923
- missingRanges = remaining;
12924
- attempt++;
12925
- continue;
12926
- }
12927
12992
  if (!madeProgress) {
12928
12993
  options.log.warn(
12929
12994
  "[Render] Capture attempt made no forward progress; composition is likely structurally broken, not retrying.",
@@ -15476,7 +15541,7 @@ import {
15476
15541
  REFERENCE_BACKGROUND_FIELD as REFERENCE_BACKGROUND_FIELD4,
15477
15542
  referenceBackgroundResult as referenceBackgroundResult6
15478
15543
  } from "@hyperframes/engine";
15479
- import { mkdirSync as mkdirSync18, mkdtempSync as mkdtempSync7, readFileSync as readFileSync13, rmSync as rmSync16 } from "node:fs";
15544
+ import { mkdirSync as mkdirSync18, mkdtempSync as mkdtempSync7, readFileSync as readFileSync14, rmSync as rmSync16 } from "node:fs";
15480
15545
  import { join as join42 } from "node:path";
15481
15546
  import { tmpdir as tmpdir4 } from "node:os";
15482
15547
 
@@ -15916,7 +15981,7 @@ import {
15916
15981
  // src/services/distributed/shared.ts
15917
15982
  import { execFile as execFileCallback } from "node:child_process";
15918
15983
  import { dirname as dirname14, join as join30 } from "node:path";
15919
- import { existsSync as existsSync19, readFileSync as readFileSync8 } from "node:fs";
15984
+ import { existsSync as existsSync19, readFileSync as readFileSync9 } from "node:fs";
15920
15985
  import { fileURLToPath as fileURLToPath5 } from "node:url";
15921
15986
  import { promisify as promisify2 } from "node:util";
15922
15987
  import {
@@ -16195,7 +16260,7 @@ function readProducerVersion() {
16195
16260
  const candidate = join30(current, "package.json");
16196
16261
  if (existsSync19(candidate)) {
16197
16262
  try {
16198
- const pkg = JSON.parse(readFileSync8(candidate, "utf-8"));
16263
+ const pkg = JSON.parse(readFileSync9(candidate, "utf-8"));
16199
16264
  if (pkg.name === "@hyperframes/producer" && typeof pkg.version === "string") {
16200
16265
  cachedProducerVersion = pkg.version;
16201
16266
  return pkg.version;
@@ -16581,7 +16646,7 @@ async function readPlanReferenceBackground(input) {
16581
16646
  }
16582
16647
 
16583
16648
  // src/services/render/stages/freezePlan.ts
16584
- import { existsSync as existsSync21, mkdirSync as mkdirSync12, readFileSync as readFileSync9, readdirSync as readdirSync7, writeFileSync as writeFileSync7 } from "node:fs";
16649
+ import { existsSync as existsSync21, mkdirSync as mkdirSync12, readFileSync as readFileSync10, readdirSync as readdirSync7, writeFileSync as writeFileSync7 } from "node:fs";
16585
16650
  import { join as join34, relative as relative3, resolve as resolve9 } from "node:path";
16586
16651
  import {
16587
16652
  REFERENCE_BACKGROUND_FIELD,
@@ -16821,7 +16886,7 @@ function collectPlanAssetShas(planDir, digests) {
16821
16886
  const assets = [];
16822
16887
  for (const file of files) {
16823
16888
  if (file.planRelativePath === COMPILED_INDEX_RELATIVE_PATH) {
16824
- compositionHtml = readFileSync9(file.absolutePath);
16889
+ compositionHtml = readFileSync10(file.absolutePath);
16825
16890
  continue;
16826
16891
  }
16827
16892
  if (HASH_EXCLUDED_PLAN_FILES.has(file.planRelativePath)) continue;
@@ -16845,7 +16910,7 @@ function writePlanRemoteFiles(planDir, files) {
16845
16910
  }
16846
16911
  function readPlanRemoteFiles(planDir) {
16847
16912
  const path = join34(planDir, PLAN_REMOTE_FILES_RELATIVE_PATH);
16848
- return existsSync21(path) ? JSON.parse(readFileSync9(path, "utf-8")) : [];
16913
+ return existsSync21(path) ? JSON.parse(readFileSync10(path, "utf-8")) : [];
16849
16914
  }
16850
16915
  function recomputePlanHashFromPlanDir(planDir, digests) {
16851
16916
  const planJsonPath = join34(planDir, "plan.json");
@@ -16856,8 +16921,8 @@ function recomputePlanHashFromPlanDir(planDir, digests) {
16856
16921
  if (!existsSync21(encoderJsonPath)) {
16857
16922
  throw new Error(`[freezePlan] meta/encoder.json missing: ${encoderJsonPath}`);
16858
16923
  }
16859
- const planJson = JSON.parse(readFileSync9(planJsonPath, "utf-8"));
16860
- const encoderConfigCanonicalJson = readFileSync9(encoderJsonPath, "utf-8");
16924
+ const planJson = JSON.parse(readFileSync10(planJsonPath, "utf-8"));
16925
+ const encoderConfigCanonicalJson = readFileSync10(encoderJsonPath, "utf-8");
16861
16926
  const { compositionHtml, assets } = collectPlanAssetShas(planDir, digests);
16862
16927
  return computePlanHash({
16863
16928
  compositionHtml,
@@ -16964,7 +17029,7 @@ function applyRuntimeEnvSnapshot(snapshot, env = process.env) {
16964
17029
  }
16965
17030
 
16966
17031
  // src/services/distributed/chunkAudio.ts
16967
- import { closeSync as closeSync6, existsSync as existsSync22, ftruncateSync, mkdirSync as mkdirSync13, openSync as openSync6, readFileSync as readFileSync10 } from "node:fs";
17032
+ import { closeSync as closeSync6, existsSync as existsSync22, ftruncateSync, mkdirSync as mkdirSync13, openSync as openSync6, readFileSync as readFileSync11 } from "node:fs";
16968
17033
  import { writeFileSync as writeFileSync8, writeSync } from "node:fs";
16969
17034
  import { join as join35 } from "node:path";
16970
17035
  import { appendRenderProvenanceArgs, describeFfmpegFailure as describeFfmpegFailure4, runFfmpeg as runFfmpeg7 } from "@hyperframes/engine";
@@ -17052,7 +17117,7 @@ async function writeChunkAudioSlices(input) {
17052
17117
  offset += track.sizes[offsets.length - 1];
17053
17118
  }
17054
17119
  });
17055
- const bytes = readFileSync10(input.audioPath);
17120
+ const bytes = readFileSync11(input.audioPath);
17056
17121
  const layout = { sizeBytes: bytes.length, payloadStart, payloadEnd, partStarts: {} };
17057
17122
  mkdirSync13(join35(input.planDir, SLICE_DIR), { recursive: true });
17058
17123
  input.chunks.forEach((_, index) => {
@@ -17071,8 +17136,8 @@ async function writeChunkAudioSlices(input) {
17071
17136
  function chunkAudioSource(planDir, chunkIndex, scratchPath) {
17072
17137
  const layoutPath = join35(planDir, SLICE_LAYOUT);
17073
17138
  if (!existsSync22(layoutPath)) return resolvePlanAudioPath(planDir);
17074
- const layout = JSON.parse(readFileSync10(layoutPath, "utf-8"));
17075
- const frame = readFileSync10(join35(planDir, SLICE_FRAME));
17139
+ const layout = JSON.parse(readFileSync11(layoutPath, "utf-8"));
17140
+ const frame = readFileSync11(join35(planDir, SLICE_FRAME));
17076
17141
  const fd = openSync6(scratchPath, "w");
17077
17142
  try {
17078
17143
  writeSync(fd, frame, 0, layout.payloadStart, 0);
@@ -17085,7 +17150,7 @@ function chunkAudioSource(planDir, chunkIndex, scratchPath) {
17085
17150
  );
17086
17151
  const start = layout.partStarts[chunkIndex];
17087
17152
  if (start !== void 0) {
17088
- const part = readFileSync10(join35(planDir, chunkAudioSlicePath(chunkIndex)));
17153
+ const part = readFileSync11(join35(planDir, chunkAudioSlicePath(chunkIndex)));
17089
17154
  writeSync(fd, part, 0, part.length, start);
17090
17155
  }
17091
17156
  ftruncateSync(fd, layout.sizeBytes);
@@ -18166,7 +18231,7 @@ async function plan(projectDir, config, planDir, options = {}) {
18166
18231
 
18167
18232
  // src/services/distributed/renderChunk.ts
18168
18233
  import { randomBytes } from "node:crypto";
18169
- import { existsSync as existsSync26, mkdirSync as mkdirSync17, readFileSync as readFileSync12, readdirSync as readdirSync10, rmSync as rmSync15, writeFileSync as writeFileSync12 } from "node:fs";
18234
+ import { existsSync as existsSync26, mkdirSync as mkdirSync17, readFileSync as readFileSync13, readdirSync as readdirSync10, rmSync as rmSync15, writeFileSync as writeFileSync12 } from "node:fs";
18170
18235
  import { cpus as cpus3 } from "node:os";
18171
18236
  import { join as join41 } from "node:path";
18172
18237
  import {
@@ -18197,7 +18262,7 @@ import {
18197
18262
  lstatSync as lstatSync3,
18198
18263
  mkdirSync as mkdirSync16,
18199
18264
  mkdtempSync as mkdtempSync6,
18200
- readFileSync as readFileSync11,
18265
+ readFileSync as readFileSync12,
18201
18266
  readdirSync as readdirSync9,
18202
18267
  rmSync as rmSync14,
18203
18268
  statSync as statSync10,
@@ -18346,7 +18411,7 @@ function isNonNegativeIntegerArray(value) {
18346
18411
  return Array.isArray(value) && value.every((item) => typeof item === "number" && Number.isInteger(item) && item >= 0);
18347
18412
  }
18348
18413
  function readJsonFile(path, label) {
18349
- const contents = readFileSync11(path, "utf-8");
18414
+ const contents = readFileSync12(path, "utf-8");
18350
18415
  try {
18351
18416
  const parsed = JSON.parse(contents);
18352
18417
  return parsed;
@@ -19180,10 +19245,10 @@ async function beginFrameSessionNeedsScreenshotFallback(session, probe = probeBe
19180
19245
  return !await probe(session.page, timeoutMs, probeTick, session.beginFrameIntervalMs);
19181
19246
  }
19182
19247
  function hashChunkOutput(outputPath, kind) {
19183
- if (kind === "file") return sha256Hex(readFileSync12(outputPath));
19248
+ if (kind === "file") return sha256Hex(readFileSync13(outputPath));
19184
19249
  const entries = readdirSync10(outputPath).filter((name) => /\.(png|jpg|jpeg)$/i.test(name)).sort();
19185
19250
  const lines = entries.map(
19186
- (name) => `${name}\0${sha256Hex(readFileSync12(join41(outputPath, name)))}`
19251
+ (name) => `${name}\0${sha256Hex(readFileSync13(join41(outputPath, name)))}`
19187
19252
  );
19188
19253
  return sha256Hex(lines.join("\0"));
19189
19254
  }
@@ -19209,7 +19274,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
19209
19274
  `[renderChunk] planDir is missing required artifact: ${planJsonPath}`
19210
19275
  );
19211
19276
  }
19212
- const plan2 = JSON.parse(readFileSync12(planJsonPath, "utf-8"));
19277
+ const plan2 = JSON.parse(readFileSync13(planJsonPath, "utf-8"));
19213
19278
  readPlanProtocolV1(plan2, DISTRIBUTED_RENDER_CAPABILITIES.roles.chunk);
19214
19279
  const planHashStarted = Date.now();
19215
19280
  const v2Materialization = readValidatedPlanV2Materialization(planDir, {
@@ -19226,13 +19291,13 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
19226
19291
  );
19227
19292
  }
19228
19293
  }
19229
- const encoder = JSON.parse(readFileSync12(encoderJsonPath, "utf-8"));
19230
- const chunks = JSON.parse(readFileSync12(chunksJsonPath, "utf-8"));
19294
+ const encoder = JSON.parse(readFileSync13(encoderJsonPath, "utf-8"));
19295
+ const chunks = JSON.parse(readFileSync13(chunksJsonPath, "utf-8"));
19231
19296
  const videosJsonPath = join41(planDir, PLAN_VIDEOS_META_RELATIVE_PATH);
19232
19297
  let planVideos = null;
19233
19298
  if (existsSync26(videosJsonPath)) {
19234
19299
  try {
19235
- planVideos = validatePlanVideosForChunk(JSON.parse(readFileSync12(videosJsonPath, "utf-8")));
19300
+ planVideos = validatePlanVideosForChunk(JSON.parse(readFileSync13(videosJsonPath, "utf-8")));
19236
19301
  } catch (err) {
19237
19302
  if (err instanceof RenderChunkValidationError) throw err;
19238
19303
  throw new RenderChunkValidationError(
@@ -19343,7 +19408,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
19343
19408
  dimensions: plan2.dimensions,
19344
19409
  encoder,
19345
19410
  videoCount: planVideos?.videos.length ?? 0,
19346
- html: readFileSync12(join41(compiledDir, "index.html"), "utf-8"),
19411
+ html: readFileSync13(join41(compiledDir, "index.html"), "utf-8"),
19347
19412
  // The plan ships a deferred source only to chunks that show it; don't wait on the rest.
19348
19413
  skipReadinessVideoIds: deferredVideos.filter(
19349
19414
  (video) => !shownFrames.get(video.videoId)?.size || remoteVideoIds.has(video.videoId)
@@ -19805,7 +19870,7 @@ async function renderSpan(projectDir, config, span, outputPath) {
19805
19870
  planned.totalFrames
19806
19871
  );
19807
19872
  const slices = JSON.parse(
19808
- readFileSync13(join42(planDir, "meta", "chunks.json"), "utf-8")
19873
+ readFileSync14(join42(planDir, "meta", "chunks.json"), "utf-8")
19809
19874
  );
19810
19875
  const parts = [];
19811
19876
  for (const slice of slices) {
@@ -20438,7 +20503,7 @@ import {
20438
20503
  // src/services/hyperframeLint.ts
20439
20504
  import {
20440
20505
  existsSync as existsSync27,
20441
- readFileSync as readFileSync14,
20506
+ readFileSync as readFileSync15,
20442
20507
  statSync as statSync11,
20443
20508
  openSync as openSync7,
20444
20509
  fstatSync as fstatSync3,
@@ -20484,7 +20549,7 @@ function readEntryHtml(filePath) {
20484
20549
  }
20485
20550
  try {
20486
20551
  if (!fstatSync3(fd).isFile()) return null;
20487
- return readFileSync14(fd, "utf-8");
20552
+ return readFileSync15(fd, "utf-8");
20488
20553
  } finally {
20489
20554
  closeSync7(fd);
20490
20555
  }
@@ -21333,7 +21398,7 @@ import {
21333
21398
  existsSync as existsSync30,
21334
21399
  mkdirSync as mkdirSync20,
21335
21400
  mkdtempSync as mkdtempSync9,
21336
- readFileSync as readFileSync15,
21401
+ readFileSync as readFileSync16,
21337
21402
  readdirSync as readdirSync11,
21338
21403
  rmSync as rmSync18,
21339
21404
  statSync as statSync13,
@@ -21442,13 +21507,13 @@ function readAssemblePlan(planDir, chunkPaths) {
21442
21507
  if (!existsSync30(planJsonPath)) {
21443
21508
  throw new Error(`[assemble] planDir missing plan.json: ${planJsonPath}`);
21444
21509
  }
21445
- const plan2 = JSON.parse(readFileSync15(planJsonPath, "utf-8"));
21510
+ const plan2 = JSON.parse(readFileSync16(planJsonPath, "utf-8"));
21446
21511
  readPlanProtocolV1(plan2, DISTRIBUTED_RENDER_CAPABILITIES.roles.assembler);
21447
21512
  validatePlanV2MaterializedTarget(planDir, { role: "assembler" });
21448
21513
  if (!existsSync30(chunksJsonPath)) {
21449
21514
  throw new Error(`[assemble] planDir missing meta/chunks.json: ${chunksJsonPath}`);
21450
21515
  }
21451
- const chunks = JSON.parse(readFileSync15(chunksJsonPath, "utf-8"));
21516
+ const chunks = JSON.parse(readFileSync16(chunksJsonPath, "utf-8"));
21452
21517
  if (chunkPaths.length !== chunks.length) {
21453
21518
  throw new Error(
21454
21519
  `[assemble] chunkPaths length (${chunkPaths.length}) does not match chunks.json length (${chunks.length}). Adapters must pass one path per chunk, ordered by index.`
@@ -21594,13 +21659,13 @@ function stitchesSeparateAudio(planDir) {
21594
21659
  return lockedAudio(planDir).separateAudio === true;
21595
21660
  }
21596
21661
  function planFormat(planDir) {
21597
- const plan2 = JSON.parse(readFileSync15(join46(planDir, "plan.json"), "utf-8"));
21662
+ const plan2 = JSON.parse(readFileSync16(join46(planDir, "plan.json"), "utf-8"));
21598
21663
  return plan2.dimensions.format;
21599
21664
  }
21600
21665
  function lockedAudio(planDir) {
21601
21666
  const encoderJsonPath = join46(planDir, "meta", "encoder.json");
21602
21667
  if (!existsSync30(encoderJsonPath)) return {};
21603
- return JSON.parse(readFileSync15(encoderJsonPath, "utf-8"));
21668
+ return JSON.parse(readFileSync16(encoderJsonPath, "utf-8"));
21604
21669
  }
21605
21670
  function readCfrEncoder(planDir, format) {
21606
21671
  if (format !== "mp4") {
@@ -21612,7 +21677,7 @@ function readCfrEncoder(planDir, format) {
21612
21677
  if (!existsSync30(encoderJsonPath)) {
21613
21678
  throw new Error(`[assemble] planDir missing meta/encoder.json: ${encoderJsonPath}`);
21614
21679
  }
21615
- const encoderJson = JSON.parse(readFileSync15(encoderJsonPath, "utf-8"));
21680
+ const encoderJson = JSON.parse(readFileSync16(encoderJsonPath, "utf-8"));
21616
21681
  if (encoderJson.encoder === "libx265-software") {
21617
21682
  throw new Error(
21618
21683
  `[assemble] cfr=true is not yet supported with codec: "h265". The cfr re-encode pass uses libx264 and would silently transcode the h265 chunks. Either disable cfr or render with codec: "h264".`