@hyperframes/producer 0.8.169 → 0.8.171

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
@@ -13969,6 +13969,13 @@ var PLAN_AUDIO_LEGACY_RELATIVE_PATH = "audio.aac";
13969
13969
  function isPlanAudioArtifactPath(path) {
13970
13970
  return path === PLAN_AUDIO_RELATIVE_PATH || path === PLAN_AUDIO_LEGACY_RELATIVE_PATH;
13971
13971
  }
13972
+ function resolvePlanAudioPath(planDir) {
13973
+ for (const name of [PLAN_AUDIO_RELATIVE_PATH, PLAN_AUDIO_LEGACY_RELATIVE_PATH]) {
13974
+ const candidate = join28(planDir, name);
13975
+ if (existsSync18(candidate)) return candidate;
13976
+ }
13977
+ return null;
13978
+ }
13972
13979
  var INVALID_VIDEO_METADATA = "INVALID_VIDEO_METADATA";
13973
13980
  var PlanVideosMetadataError = class extends Error {
13974
13981
  // Read by cloud adapters across the package boundary to classify retries.
@@ -14621,9 +14628,28 @@ function buildLockedRenderConfig(input) {
14621
14628
  chunkSize: input.effectiveChunkSize,
14622
14629
  chunkCount: input.chunkCount,
14623
14630
  runtimeEnv: input.runtimeEnv,
14624
- variables: config.variables
14631
+ variables: config.variables,
14632
+ ...input.chunksCarryAudio ? { chunksCarryAudio: true } : {}
14625
14633
  };
14626
14634
  }
14635
+ function carriesChunkAudio(config, options) {
14636
+ return options.chunksCarryAudio === true && config.format === "mp4" && config.cfr !== true;
14637
+ }
14638
+ async function fitPlanAudioToFrames(audioPath, workDir, totalFrames, fps, run) {
14639
+ const fittedPath = join30(workDir, "audio-fitted.m4a");
14640
+ const result = await padOrTrimAudioToVideoFrameCount({
14641
+ videoPath: audioPath,
14642
+ probeVideoFrameInfo: async () => ({ frameCount: totalFrames, fpsNum: fps, fpsDen: 1 }),
14643
+ audioPath,
14644
+ outputPath: fittedPath,
14645
+ signal: run.signal,
14646
+ timeoutMs: run.timeoutMs
14647
+ });
14648
+ if (!result.success) {
14649
+ throw encoderFailureError("[plan] audio pad/trim failed", result);
14650
+ }
14651
+ renameSync5(fittedPath, audioPath);
14652
+ }
14627
14653
  function resolveEncoderTriple(config) {
14628
14654
  if (config.format === "mp4") {
14629
14655
  return resolveMp4EncoderTriple(config.codec);
@@ -14677,6 +14703,13 @@ function pathInside(root, path) {
14677
14703
  const rel = relative5(root, path);
14678
14704
  return rel === "" || rel.startsWith("..") || isAbsolute3(rel) ? null : rel;
14679
14705
  }
14706
+ function stableSourcePath(roots, srcPath) {
14707
+ for (const [root, prefix] of roots) {
14708
+ const rel = pathInside(root, srcPath);
14709
+ if (rel !== null) return [prefix, ...rel.split(sep2)].filter(Boolean).join("/");
14710
+ }
14711
+ return srcPath;
14712
+ }
14680
14713
  function createVideoSourceShipper(input) {
14681
14714
  const shipped = /* @__PURE__ */ new Map();
14682
14715
  return (srcPath) => {
@@ -14889,12 +14922,17 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
14889
14922
  workCompiledDir: compiledDir,
14890
14923
  workVideoFramesDir: stagedVideoFrames
14891
14924
  });
14925
+ const sourceRoots = [
14926
+ [stagedVideoFrames, "video-frames"],
14927
+ [compiledDir, "compiled"],
14928
+ [projectDir, ""]
14929
+ ];
14892
14930
  const planVideosJson = buildPlanVideosJson({
14893
14931
  videos: composition.videos,
14894
14932
  compositionEnd: job.duration ?? Number.NaN,
14895
14933
  extracted: (extractResult.extractionResult?.extracted ?? []).map((ext) => ({
14896
14934
  videoId: ext.videoId,
14897
- srcPath: ext.srcPath,
14935
+ srcPath: stableSourcePath(sourceRoots, ext.srcPath),
14898
14936
  framePattern: ext.framePattern,
14899
14937
  fps: ext.fps,
14900
14938
  totalFrames: ext.totalFrames,
@@ -14945,6 +14983,13 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
14945
14983
  if (audioResult.hasAudio && existsSync19(audioResult.audioOutputPath)) {
14946
14984
  renameSync5(audioResult.audioOutputPath, planAudioPath);
14947
14985
  }
14986
+ const chunksCarryAudio2 = carriesChunkAudio(config, options);
14987
+ if (chunksCarryAudio2 && existsSync19(planAudioPath)) {
14988
+ await fitPlanAudioToFrames(planAudioPath, workDir, totalFrames, config.fps, {
14989
+ signal: abortSignal,
14990
+ timeoutMs: cfg.ffmpegProcessTimeout
14991
+ });
14992
+ }
14948
14993
  const ffmpegVersion = await readFfmpegVersion();
14949
14994
  const producerVersion = readProducerVersion();
14950
14995
  const fontSnapshotSha = await readFontSnapshotSha();
@@ -14957,7 +15002,8 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
14957
15002
  engineConfig: cfg,
14958
15003
  effectiveChunkSize,
14959
15004
  chunkCount,
14960
- runtimeEnv
15005
+ runtimeEnv,
15006
+ chunksCarryAudio: chunksCarryAudio2
14961
15007
  });
14962
15008
  const compositionJson = {
14963
15009
  durationSeconds: job.duration ?? 0,
@@ -16656,8 +16702,10 @@ function resolveExtractedVideoOutputDir(planDir, videoId) {
16656
16702
  }
16657
16703
  return outputDir;
16658
16704
  }
16659
- function artifactTargets(path, videoDependencies) {
16660
- if (isPlanAudioArtifactPath(path)) return { chunks: [], assembler: true };
16705
+ function artifactTargets(path, videoDependencies, chunksCarryAudio2) {
16706
+ if (isPlanAudioArtifactPath(path)) {
16707
+ return { chunks: chunksCarryAudio2 ? "all" : [], assembler: true };
16708
+ }
16661
16709
  if (path === "plan.json" || path === "meta/chunks.json" || path === "meta/encoder.json") {
16662
16710
  return { chunks: "all", assembler: true };
16663
16711
  }
@@ -16879,9 +16927,11 @@ function buildPlanV2Publication(executionPlanDir, digests) {
16879
16927
  }
16880
16928
  validateExtractionCacheCompleteSentinels(executionPlanDir);
16881
16929
  const videoDependencyPlan = buildVideoChunkDependencies(executionPlanDir, dimensions);
16930
+ const encoder = readJsonFile(join36(executionPlanDir, "meta", "encoder.json"), "meta/encoder.json");
16931
+ const chunksCarryAudio2 = isRecord3(encoder) && encoder.chunksCarryAudio === true;
16882
16932
  for (const file of listFiles(executionPlanDir)) {
16883
16933
  if (isExtractionCacheCompleteSentinelPath(file.path)) continue;
16884
- const targets = artifactTargets(file.path, videoDependencyPlan.dependencies);
16934
+ const targets = artifactTargets(file.path, videoDependencyPlan.dependencies, chunksCarryAudio2);
16885
16935
  if ((file.path.startsWith("video-frames/") || file.path.startsWith("video-sources/")) && targets.chunks !== "all" && targets.chunks.length === 0 && !targets.assembler) {
16886
16936
  continue;
16887
16937
  }
@@ -16984,7 +17034,8 @@ async function planV2WithPublisher(projectDir, config, publisher, options = {})
16984
17034
  listFiles(join36(stagingRoot, "compiled")).map(
16985
17035
  (file) => publishBlobFor(file.absolutePath, digests)
16986
17036
  )
16987
- )
17037
+ ),
17038
+ chunksCarryAudio: options.chunksCarryAudio
16988
17039
  });
16989
17040
  return await publishExecutionPlan(stagingRoot, publisher, { digests, log: config.logger });
16990
17041
  } catch (error) {
@@ -17307,6 +17358,7 @@ async function assemble(planDir, chunkPaths, audioPath, outputPath, options) {
17307
17358
  format,
17308
17359
  fps,
17309
17360
  fpsArg: fpsToFfmpegArg3(fps),
17361
+ chunkSeconds: chunksCarryAudio(planDir) ? plan2.chunks.map((chunk) => (chunk.endFrame - chunk.startFrame) * fps.den / fps.num) : null,
17310
17362
  signal: options?.abortSignal,
17311
17363
  timeout: resolveConfig6().ffmpegProcessTimeout,
17312
17364
  log,
@@ -17369,10 +17421,10 @@ function readAssemblePlan(planDir, chunkPaths) {
17369
17421
  throw new Error(`[assemble] chunk path does not exist: ${path}`);
17370
17422
  }
17371
17423
  }
17372
- return plan2;
17424
+ return { ...plan2, chunks };
17373
17425
  }
17374
17426
  function videoInputArgs(ctx, videoSources) {
17375
- return videoSources.length === 1 ? ["-i", videoSources[0]] : concatDemuxerInput(videoSources, ctx.workDir, ctx.fpsArg);
17427
+ return videoSources.length === 1 ? ["-i", videoSources[0]] : concatDemuxerInput(videoSources, ctx.workDir, ctx.fpsArg, ctx.chunkSeconds);
17376
17428
  }
17377
17429
  async function reencodeCfr(ctx, chunkPaths, encoder) {
17378
17430
  const cfrOutputPath = join37(ctx.workDir, `cfr.${ctx.format}`);
@@ -17452,10 +17504,15 @@ async function writeJoined(ctx, videoSources, audioPath, finalPath) {
17452
17504
  throw encoderFailureError("[assemble] audio mux failed", muxResult);
17453
17505
  }
17454
17506
  }
17455
- function concatDemuxerInput(chunkPaths, workDir, fpsArg) {
17507
+ function concatDemuxerInput(chunkPaths, workDir, fpsArg, chunkSeconds) {
17456
17508
  const listPath = join37(workDir, "concat-list.txt");
17457
- const body = chunkPaths.map((path) => `file '${path.replace(/'/g, "'\\''")}'`).join("\n");
17458
- writeFileSync11(listPath, `${body}
17509
+ const head = chunkSeconds ? ["stream", "exact_stream_id 1"] : [];
17510
+ const body = chunkPaths.map((path, i) => {
17511
+ const line = `file '${path.replace(/'/g, "'\\''")}'`;
17512
+ return chunkSeconds ? `${line}
17513
+ duration ${chunkSeconds[i].toFixed(6)}` : line;
17514
+ });
17515
+ writeFileSync11(listPath, `${[...head, ...body].join("\n")}
17459
17516
  `, "utf-8");
17460
17517
  return ["-r", fpsArg, "-f", "concat", "-safe", "0", "-i", listPath];
17461
17518
  }
@@ -17478,6 +17535,11 @@ async function runAssembleFfmpeg(args, step, signal, timeout) {
17478
17535
  });
17479
17536
  }
17480
17537
  }
17538
+ function chunksCarryAudio(planDir) {
17539
+ const encoderJsonPath = join37(planDir, "meta", "encoder.json");
17540
+ if (!existsSync25(encoderJsonPath)) return false;
17541
+ return JSON.parse(readFileSync13(encoderJsonPath, "utf-8")).chunksCarryAudio === true;
17542
+ }
17481
17543
  function readCfrEncoder(planDir, format) {
17482
17544
  if (format !== "mp4") {
17483
17545
  throw new Error(
@@ -17562,6 +17624,104 @@ import {
17562
17624
  readWebGlVendorInfoFromCanvas,
17563
17625
  resolveConfig as resolveConfig7
17564
17626
  } from "@hyperframes/engine";
17627
+
17628
+ // src/services/distributed/chunkAudio.ts
17629
+ import { appendRenderProvenanceArgs as appendRenderProvenanceArgs2, describeFfmpegFailure as describeFfmpegFailure3, runFfmpeg as runFfmpeg6 } from "@hyperframes/engine";
17630
+ function audioCutForChunk(grid, chunks, index, fps) {
17631
+ const firstPacketAt = (frame) => Math.min(
17632
+ grid.packetCount,
17633
+ Math.max(
17634
+ 0,
17635
+ Math.ceil(
17636
+ (frame * fps.den * grid.sampleRate - grid.firstStart * fps.num) / (grid.packetSamples * fps.num)
17637
+ )
17638
+ )
17639
+ );
17640
+ const first = index === 0 ? 0 : firstPacketAt(chunks[index].startFrame);
17641
+ const end = index === chunks.length - 1 ? grid.packetCount : firstPacketAt(chunks[index + 1].startFrame);
17642
+ if (end <= first) return null;
17643
+ const cutSeconds = (packet) => (grid.firstStart + packet * grid.packetSamples - grid.packetSamples / 2) / grid.sampleRate;
17644
+ const seekSeconds = index === 0 ? null : cutSeconds(first);
17645
+ const durationSeconds = end === grid.packetCount ? null : cutSeconds(end) - (seekSeconds ?? 0);
17646
+ return { seekSeconds, durationSeconds, firstPacket: first, packetCount: end - first };
17647
+ }
17648
+ async function probeAacPacketGrid(audioPath) {
17649
+ const probe = await runFfprobeJson([
17650
+ "-v",
17651
+ "error",
17652
+ "-select_streams",
17653
+ "a:0",
17654
+ "-show_entries",
17655
+ "stream=sample_rate,time_base,nb_frames:packet=pts,duration",
17656
+ "-read_intervals",
17657
+ "%+#1",
17658
+ "-of",
17659
+ "json",
17660
+ "--",
17661
+ audioPath
17662
+ ]);
17663
+ const stream = probe.streams?.[0] ?? {};
17664
+ const packet = probe.packets?.[0] ?? {};
17665
+ const sampleRate = Number(stream.sample_rate);
17666
+ const [tbNum = NaN, tbDen = NaN] = String(stream.time_base).split("/").map(Number);
17667
+ const toSamples = (ticks) => Number(ticks) * sampleRate * tbNum / tbDen;
17668
+ const grid = {
17669
+ sampleRate,
17670
+ firstStart: toSamples(packet.pts),
17671
+ packetSamples: toSamples(packet.duration),
17672
+ packetCount: Number(stream.nb_frames)
17673
+ };
17674
+ const counts = [grid.sampleRate, grid.packetSamples, grid.packetCount];
17675
+ if (!Number.isFinite(grid.firstStart) || !counts.every((n) => Number.isFinite(n) && n > 0)) {
17676
+ throw new Error(`[chunkAudio] could not read the AAC packet grid: ${JSON.stringify(probe)}`);
17677
+ }
17678
+ return grid;
17679
+ }
17680
+ function chunkMuxArgs(input, cut) {
17681
+ if (!cut) return ["-i", input.videoPath, "-map", "0:v:0"];
17682
+ const seek = cut.seekSeconds === null ? [] : ["-ss", cut.seekSeconds.toFixed(6)];
17683
+ const length = cut.durationSeconds === null ? [] : ["-t", cut.durationSeconds.toFixed(6)];
17684
+ const exact = seek.length ? ["-copypriorss", "0"] : [];
17685
+ return [
17686
+ ...["-i", input.videoPath, ...seek, ...length, "-i", input.audioPath],
17687
+ ...["-map", "0:v:0", "-map", "1:a:0", ...exact]
17688
+ ];
17689
+ }
17690
+ async function muxChunkWithAudio(input) {
17691
+ const grid = input.audioPath ? await probeAacPacketGrid(input.audioPath) : null;
17692
+ const cut = grid ? audioCutForChunk(grid, input.chunks, input.chunkIndex, input.fps) : null;
17693
+ const args = [...chunkMuxArgs(input, cut), "-c", "copy"];
17694
+ appendRenderProvenanceArgs2(args, input.outputPath);
17695
+ args.push("-y", input.outputPath);
17696
+ const result = await runFfmpeg6(args, { signal: input.signal, timeout: input.timeoutMs });
17697
+ if (!result.success) {
17698
+ throw new Error(
17699
+ `[chunkAudio] chunk mux failed: ${describeFfmpegFailure3(result, input.timeoutMs)}`
17700
+ );
17701
+ }
17702
+ const slice = input.chunks[input.chunkIndex];
17703
+ const expected = { video: slice.endFrame - slice.startFrame };
17704
+ if (cut) expected.audio = cut.packetCount;
17705
+ const probe = await runFfprobeJson([
17706
+ "-v",
17707
+ "error",
17708
+ "-count_packets",
17709
+ "-show_entries",
17710
+ "stream=codec_type,nb_read_packets",
17711
+ "-of",
17712
+ "json",
17713
+ "--",
17714
+ input.outputPath
17715
+ ]);
17716
+ for (const [type, count] of Object.entries(expected)) {
17717
+ const packets = Number(probe.streams?.find((s) => s.codec_type === type)?.nb_read_packets ?? 0);
17718
+ if (packets !== count) {
17719
+ throw new Error(`[chunkAudio] ${type} packets in chunk ${packets}, expected ${count}`);
17720
+ }
17721
+ }
17722
+ }
17723
+
17724
+ // src/services/distributed/renderChunk.ts
17565
17725
  import { readWebGlVendorInfoFromCanvas as readWebGlVendorInfoFromCanvas2 } from "@hyperframes/engine";
17566
17726
  var FFMPEG_VERSION_MISMATCH = "FFMPEG_VERSION_MISMATCH";
17567
17727
  var PLAN_HASH_MISMATCH = "PLAN_HASH_MISMATCH";
@@ -18111,7 +18271,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
18111
18271
  const preset = resolvePresetForLockedEncoder(basePreset, encoder.encoder);
18112
18272
  const effectiveQuality = encoder.crf ?? preset.quality;
18113
18273
  const effectiveBitrate = encoder.crf != null ? void 0 : encoder.bitrate;
18114
- const videoOnlyPath = outputChunkPath;
18274
+ const videoOnlyPath = encoder.chunksCarryAudio ? `${outputChunkPath}.video.mp4` : outputChunkPath;
18115
18275
  if (isPngSequence) {
18116
18276
  if (!existsSync26(outputChunkPath)) mkdirSync16(outputChunkPath, { recursive: true });
18117
18277
  } else {
@@ -18122,16 +18282,16 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
18122
18282
  await runEncodeStage({
18123
18283
  job,
18124
18284
  log,
18125
- outputPath: outputChunkPath,
18285
+ outputPath: videoOnlyPath,
18126
18286
  framesDir,
18127
18287
  videoOnlyPath,
18128
18288
  width: Math.round(plan2.dimensions.width * encoder.deviceScaleFactor),
18129
18289
  height: Math.round(plan2.dimensions.height * encoder.deviceScaleFactor),
18130
18290
  needsAlpha: plan2.dimensions.format !== "mp4",
18131
18291
  captureImageFormat: captureOptions.format ?? "jpeg",
18132
- // Each chunk produces video only — audio is muxed once at assemble
18133
- // time. Suppressing `hasAudio` skips the png-sequence audio sidecar
18134
- // AND the mp4 audio mux.
18292
+ // The encoder writes video only; audio joins at assemble time or in the
18293
+ // chunk that carries audio. Suppressing `hasAudio` skips the png-sequence audio
18294
+ // sidecar AND the mp4 audio mux.
18135
18295
  hasAudio: false,
18136
18296
  isPngSequence,
18137
18297
  // `DistributedFormat` has no "gif" member — distributed chunks are
@@ -18158,6 +18318,18 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
18158
18318
  lockGopForChunkConcat: !isPngSequence,
18159
18319
  gopSize: framesInChunk
18160
18320
  });
18321
+ if (encoder.chunksCarryAudio) {
18322
+ await muxChunkWithAudio({
18323
+ videoPath: videoOnlyPath,
18324
+ audioPath: resolvePlanAudioPath(planDir),
18325
+ outputPath: outputChunkPath,
18326
+ chunks,
18327
+ chunkIndex,
18328
+ fps: { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen },
18329
+ timeoutMs: cfg.ffmpegProcessTimeout
18330
+ });
18331
+ rmSync15(videoOnlyPath, { force: true });
18332
+ }
18161
18333
  encodeStageMs = Date.now() - encodeStarted;
18162
18334
  } finally {
18163
18335
  if (session) {
@@ -0,0 +1,30 @@
1
+ import type { Fps } from "@hyperframes/core";
2
+ import type { ChunkSliceJson } from "../render/stages/freezePlan.js";
3
+ /** Where AAC packet `j` of the plan audio starts: `firstStart + j * packetSamples`, in samples. */
4
+ interface AacPacketGrid {
5
+ sampleRate: number;
6
+ firstStart: number;
7
+ packetSamples: number;
8
+ packetCount: number;
9
+ }
10
+ /** Input-side `-ss` / `-t` that select exactly packets `[first, end)` for one chunk. */
11
+ interface AudioCut {
12
+ seekSeconds: number | null;
13
+ durationSeconds: number | null;
14
+ firstPacket: number;
15
+ packetCount: number;
16
+ }
17
+ export declare function audioCutForChunk(grid: AacPacketGrid, chunks: readonly ChunkSliceJson[], index: number, fps: Fps): AudioCut | null;
18
+ interface MuxInput {
19
+ videoPath: string;
20
+ audioPath: string | null;
21
+ outputPath: string;
22
+ chunks: readonly ChunkSliceJson[];
23
+ chunkIndex: number;
24
+ fps: Fps;
25
+ signal?: AbortSignal;
26
+ timeoutMs: number;
27
+ }
28
+ /** Mux one encoded chunk with the plan audio packets it owns into a regular mp4 at `outputPath`. */
29
+ export declare function muxChunkWithAudio(input: MuxInput): Promise<void>;
30
+ export {};
@@ -205,6 +205,11 @@ export interface BuildLocalExecutionPlanOptions {
205
205
  readonly digests?: PlanFileDigests;
206
206
  /** Runs while the audio mixes, once `compiled/` and the video trees are final. */
207
207
  readonly whileAudioMixes?: () => Promise<void>;
208
+ /**
209
+ * mp4 chunks carry their own slice of the plan audio (renderChunk muxes it). Ignored for other
210
+ * formats and with `cfr`, which re-encodes.
211
+ */
212
+ readonly chunksCarryAudio?: boolean;
208
213
  }
209
214
  export declare function logPlanStage(log: ProducerLogger, stage: string, startedAt: number): void;
210
215
  /**
@@ -88,6 +88,7 @@ export interface PlanV2WithPublisherOptions {
88
88
  * can avoid a second set of data blocks.
89
89
  */
90
90
  readonly stagingParentDir?: string;
91
+ readonly chunksCarryAudio?: boolean;
91
92
  }
92
93
  export interface PlanV2MaterializationResult {
93
94
  readonly planDir: string;
@@ -112,3 +112,4 @@ export declare function buildPadTrimAudioArgs(audioPath: string, outputPath: str
112
112
  */
113
113
  export declare function padOrTrimAudioToVideoFrameCount(input: PadTrimAudioInput): Promise<PadTrimAudioResult>;
114
114
  export declare function defaultProbeVideoFrameInfo(videoPath: string, signal?: AbortSignal): Promise<ProbeVideoFrameInfo>;
115
+ export declare function runFfprobeJson<T>(args: string[], signal?: AbortSignal): Promise<T>;
@@ -69,6 +69,7 @@ export interface LockedRenderConfig {
69
69
  * before this field existed.
70
70
  */
71
71
  variables?: Record<string, unknown>;
72
+ chunksCarryAudio?: true;
72
73
  }
73
74
  export interface CompositionMetadataJson {
74
75
  durationSeconds: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/producer",
3
- "version": "0.8.169",
3
+ "version": "0.8.171",
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/core": "0.8.169",
39
- "@hyperframes/engine": "0.8.169",
40
- "@hyperframes/lint": "0.8.169",
41
- "@hyperframes/parsers": "0.8.169",
42
- "@hyperframes/studio-server": "0.8.169"
38
+ "@hyperframes/core": "0.8.171",
39
+ "@hyperframes/lint": "0.8.171",
40
+ "@hyperframes/engine": "0.8.171",
41
+ "@hyperframes/studio-server": "0.8.171",
42
+ "@hyperframes/parsers": "0.8.171"
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.169"
51
+ "@hyperframes/aws-lambda": "0.8.171"
52
52
  },
53
53
  "engines": {
54
54
  "node": ">=22"