@hyperframes/producer 0.8.204 → 0.8.206

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.
@@ -3,14 +3,14 @@
3
3
  // src/server.ts
4
4
  import {
5
5
  existsSync as existsSync19,
6
- mkdirSync as mkdirSync10,
7
- statSync as statSync9,
6
+ mkdirSync as mkdirSync11,
7
+ statSync as statSync10,
8
8
  mkdtempSync as mkdtempSync4,
9
- writeFileSync as writeFileSync6,
10
- rmSync as rmSync12,
9
+ writeFileSync as writeFileSync7,
10
+ rmSync as rmSync13,
11
11
  createReadStream as createReadStream3
12
12
  } from "node:fs";
13
- import { resolve as resolve9, dirname as dirname12, join as join30 } from "node:path";
13
+ import { resolve as resolve10, dirname as dirname13, join as join31 } from "node:path";
14
14
  import { tmpdir as tmpdir3 } from "node:os";
15
15
  import { parseArgs } from "node:util";
16
16
  import crypto from "node:crypto";
@@ -23,13 +23,13 @@ import { fetchPublicHttpsText as fetchPublicHttpsText2 } from "@hyperframes/engi
23
23
  import { audioFailureDiagnostics as audioFailureDiagnostics2 } from "@hyperframes/engine";
24
24
  import {
25
25
  existsSync as existsSync16,
26
- mkdirSync as mkdirSync9,
27
- readFileSync as readFileSync8,
26
+ mkdirSync as mkdirSync10,
27
+ readFileSync as readFileSync9,
28
28
  readdirSync as readdirSync5,
29
- rmSync as rmSync11,
30
- statSync as statSync7,
31
- writeFileSync as writeFileSync5,
32
- copyFileSync as copyFileSync4,
29
+ rmSync as rmSync12,
30
+ statSync as statSync8,
31
+ writeFileSync as writeFileSync6,
32
+ copyFileSync as copyFileSync5,
33
33
  appendFileSync
34
34
  } from "fs";
35
35
  import { tmpdir as tmpdir2 } from "node:os";
@@ -77,7 +77,7 @@ import {
77
77
  augmentProtocolTimeoutError,
78
78
  augmentPageNavigationTimeoutError
79
79
  } from "@hyperframes/engine";
80
- import { join as join26, dirname as dirname10, resolve as resolve7 } from "path";
80
+ import { join as join27, dirname as dirname11, resolve as resolve8 } from "path";
81
81
  import { totalmem as totalmem2 } from "node:os";
82
82
  import { randomUUID } from "crypto";
83
83
  import { fileURLToPath as fileURLToPath4 } from "url";
@@ -101,6 +101,7 @@ import { Readable } from "node:stream";
101
101
  import { join, extname, resolve as resolve2, sep } from "node:path";
102
102
  import { injectScriptsAtHeadStart, injectScriptsIntoHtml } from "@hyperframes/core/compiler";
103
103
  import { fpsToNumber } from "@hyperframes/core";
104
+ import { stripSdrPngColourTags } from "@hyperframes/core/compiler/png-colour-tags";
104
105
 
105
106
  // src/services/hyperframeRuntimeLoader.ts
106
107
  import { createHash } from "node:crypto";
@@ -217,11 +218,11 @@ var defaultLogger = createConsoleLogger("info");
217
218
 
218
219
  // src/services/fileServer.ts
219
220
  var readFileAsync = promisify(readFile);
220
- function isPathInside(child, parent, options = {}) {
221
+ function isPathInside(child2, parent, options = {}) {
221
222
  const { resolveSymlinks = false, pathModule } = options;
222
223
  const resolveFn = pathModule?.resolve ?? resolve2;
223
224
  const separator = pathModule?.sep ?? sep;
224
- const resolvedChild = resolveFn(child);
225
+ const resolvedChild = resolveFn(child2);
225
226
  const resolvedParent = resolveFn(parent);
226
227
  const normalizedChild = resolveSymlinks && existsSync2(resolvedChild) ? realpathSync.native(resolvedChild) : resolvedChild;
227
228
  const normalizedParent = resolveSymlinks && existsSync2(resolvedParent) ? realpathSync.native(resolvedParent) : resolvedParent;
@@ -741,6 +742,18 @@ function createFileServer(options) {
741
742
  html = isIndex ? injectScriptsIntoHtml(html, headScripts, bodyScripts, stripEmbeddedRuntime) : html;
742
743
  return c.text(html, 200, { "Content-Type": contentType });
743
744
  }
745
+ if (ext === ".png") {
746
+ const untagged = stripSdrPngColourTags(await readFileAsync(fd));
747
+ if (untagged) {
748
+ const headers = {
749
+ "Content-Type": contentType,
750
+ "Content-Length": String(untagged.length)
751
+ };
752
+ return new Response(c.req.method === "HEAD" ? null : new Uint8Array(untagged), {
753
+ headers
754
+ });
755
+ }
756
+ }
744
757
  const totalSize = stat.size;
745
758
  const rangeHeader = c.req.header("range");
746
759
  const rangeRequest = parseRangeHeader(rangeHeader, totalSize);
@@ -768,7 +781,8 @@ function createFileServer(options) {
768
781
  const stream = createReadStream(filePath, {
769
782
  fd,
770
783
  autoClose: true,
771
- ...range ? { start: range.start, end: range.end } : {}
784
+ start: range?.start ?? 0,
785
+ ...range ? { end: range.end } : {}
772
786
  });
773
787
  streamOwnsFd = true;
774
788
  try {
@@ -782,10 +796,10 @@ function createFileServer(options) {
782
796
  if (!streamOwnsFd) closeSync(fd);
783
797
  }
784
798
  });
785
- return new Promise((resolve10) => {
799
+ return new Promise((resolve11) => {
786
800
  const connections = /* @__PURE__ */ new Set();
787
801
  const server = serve({ fetch: app.fetch, port, hostname: "127.0.0.1" }, (info) => {
788
- resolve10({
802
+ resolve11({
789
803
  url: `http://localhost:${info.port}`,
790
804
  port: info.port,
791
805
  addPreHeadScript: (script) => {
@@ -1292,8 +1306,8 @@ var OrderedRenderEventPublisher = class {
1292
1306
  let timer;
1293
1307
  const drained = await Promise.race([
1294
1308
  this.tail.then(() => true),
1295
- new Promise((resolve10) => {
1296
- timer = setTimeout(() => resolve10(false), FLUSH_TIMEOUT_MS);
1309
+ new Promise((resolve11) => {
1310
+ timer = setTimeout(() => resolve11(false), FLUSH_TIMEOUT_MS);
1297
1311
  })
1298
1312
  ]);
1299
1313
  clearTimeout(timer);
@@ -3636,7 +3650,7 @@ var SequentialCaptureStallError = class extends Error {
3636
3650
  }
3637
3651
  };
3638
3652
  function raceAgainstStall(promise, deadlineMs, input, signal) {
3639
- return new Promise((resolve10, reject) => {
3653
+ return new Promise((resolve11, reject) => {
3640
3654
  const timer = setTimeout(
3641
3655
  () => {
3642
3656
  reject(
@@ -3655,7 +3669,7 @@ function raceAgainstStall(promise, deadlineMs, input, signal) {
3655
3669
  promise.then(
3656
3670
  (value) => {
3657
3671
  clearTimeout(timer);
3658
- resolve10(value);
3672
+ resolve11(value);
3659
3673
  },
3660
3674
  (error) => {
3661
3675
  clearTimeout(timer);
@@ -5583,15 +5597,15 @@ function countAuthoredTimedClips(html) {
5583
5597
  }
5584
5598
 
5585
5599
  // src/services/render/stages/compileStage.ts
5586
- import { join as join15 } from "node:path";
5600
+ import { join as join16 } from "node:path";
5587
5601
  import { suggestMatchingPreset } from "@hyperframes/core";
5588
5602
 
5589
5603
  // src/services/htmlCompiler.ts
5590
5604
  import { decodedUrlPath } from "@hyperframes/parsers/asset-paths";
5591
- import { createHash as createHash5 } from "node:crypto";
5592
- import { createReadStream as createReadStream2, existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync6 } from "fs";
5593
- import { join as join14, dirname as dirname6, resolve as resolve6, basename as basename4, relative as relative2 } from "path";
5594
- import { parseHTML as parseHTML4 } from "linkedom";
5605
+ import { createHash as createHash6 } from "node:crypto";
5606
+ import { createReadStream as createReadStream2, existsSync as existsSync10, mkdirSync as mkdirSync7, readFileSync as readFileSync7 } from "fs";
5607
+ import { join as join15, dirname as dirname7, resolve as resolve7, basename as basename4, relative as relative2 } from "path";
5608
+ import { parseHTML as parseHTML5 } from "linkedom";
5595
5609
  import {
5596
5610
  COMPOSITION_SOURCE_URL,
5597
5611
  compileTimingAttrs,
@@ -5629,6 +5643,8 @@ import {
5629
5643
  readDeclaredDefaults,
5630
5644
  parseHostVariableValues,
5631
5645
  headStyleRuns,
5646
+ isAlwaysAppliedStyle,
5647
+ dedupeFontFaceRules,
5632
5648
  inlineScriptRuns,
5633
5649
  parsesAsScript,
5634
5650
  styleElementsFor,
@@ -5865,7 +5881,7 @@ function readLoopOverride(el) {
5865
5881
  }
5866
5882
  return null;
5867
5883
  }
5868
- function resolveGifSourcePath(src, options) {
5884
+ function resolveLocalImageSourcePath(src, options) {
5869
5885
  const trimmed = src.trim();
5870
5886
  if (!trimmed || trimmed.startsWith("data:")) return null;
5871
5887
  const { basePath } = splitUrlSuffix(trimmed);
@@ -5921,12 +5937,12 @@ function resolveClipWindowSeconds(img, compositionDuration) {
5921
5937
  }
5922
5938
  return null;
5923
5939
  }
5924
- function resolvePreparedPlayback(metadata, loop, windowSeconds) {
5940
+ function resolvePreparedPlayback(metadata, loopOverride, windowSeconds) {
5925
5941
  const gifDuration = metadata.durationSeconds > 0 ? metadata.durationSeconds : null;
5926
5942
  let loopIterations = 1;
5927
- if (loop) {
5943
+ if (resolveLoop(metadata, loopOverride)) {
5928
5944
  loopIterations = windowSeconds != null && gifDuration != null ? Math.min(MAX_LOOP_ITERATIONS, Math.max(1, Math.ceil(windowSeconds / gifDuration))) : 1;
5929
- } else if (metadata.loopCount != null && metadata.loopCount > 1) {
5945
+ } else if (loopOverride == null && metadata.loopCount != null && metadata.loopCount > 1) {
5930
5946
  loopIterations = Math.min(MAX_LOOP_ITERATIONS, metadata.loopCount);
5931
5947
  }
5932
5948
  let padSeconds = 0;
@@ -6088,14 +6104,19 @@ async function prepareAnimatedGifInputs(html, options) {
6088
6104
  const img = images[i];
6089
6105
  const src = (img.getAttribute("src") || "").trim();
6090
6106
  if (!hasGifExtension(src)) continue;
6091
- const sourcePath = resolveGifSourcePath(src, options);
6107
+ const sourcePath = resolveLocalImageSourcePath(src, options);
6092
6108
  if (!sourcePath) continue;
6093
6109
  const bytes = readFileSync5(sourcePath);
6094
6110
  const metadata = parseAnimatedGifMetadata(bytes);
6095
6111
  if (!metadata?.animated) continue;
6096
- const loop = resolveLoop(metadata, readLoopOverride(img));
6112
+ const loopOverride = readLoopOverride(img);
6113
+ const loop = resolveLoop(metadata, loopOverride);
6097
6114
  const windowSeconds = resolveClipWindowSeconds(img, compositionDuration);
6098
- const { loopIterations, padSeconds } = resolvePreparedPlayback(metadata, loop, windowSeconds);
6115
+ const { loopIterations, padSeconds } = resolvePreparedPlayback(
6116
+ metadata,
6117
+ loopOverride,
6118
+ windowSeconds
6119
+ );
6099
6120
  const hash = computePreparedGifHash(bytes, loopIterations, padSeconds);
6100
6121
  const filename = `${CACHE_SCHEMA}-${hash.slice(0, 24)}.webm`;
6101
6122
  const cachePath = join13(cacheDir, filename);
@@ -6131,19 +6152,74 @@ async function prepareAnimatedGifInputs(html, options) {
6131
6152
  };
6132
6153
  }
6133
6154
 
6134
- // src/services/htmlCompiler.ts
6135
- import { createStudioPositionSeekReapplyScript } from "@hyperframes/studio-server/manual-edits-render-script";
6136
- import { getPositionEditsRenderScript } from "@hyperframes/core/runtime/position-edits-render";
6137
-
6138
- // src/services/assetMediaType.ts
6139
- import { existsSync as existsSync9 } from "node:fs";
6155
+ // src/services/heifImagePrep.ts
6156
+ import { createHash as createHash5 } from "node:crypto";
6140
6157
  import {
6141
- fingerprintElementId,
6142
- isNotMediaPayload,
6143
- NotMediaPayloadError,
6144
- probeMediaProfile,
6145
- resolveProjectRelativeSrc as resolveProjectRelativeSrc2
6146
- } from "@hyperframes/engine";
6158
+ closeSync as closeSync4,
6159
+ copyFileSync as copyFileSync3,
6160
+ mkdirSync as mkdirSync6,
6161
+ openSync as openSync4,
6162
+ readFileSync as readFileSync6,
6163
+ readSync as readSync3,
6164
+ rmSync as rmSync8,
6165
+ statSync as statSync6,
6166
+ writeFileSync as writeFileSync4
6167
+ } from "node:fs";
6168
+ import { dirname as dirname6, join as join14, resolve as resolve6 } from "node:path";
6169
+ import { parseHTML as parseHTML4 } from "linkedom";
6170
+ import { renameWithRetry as renameWithRetry4, runFfmpeg as runFfmpeg3, runFfprobe } from "@hyperframes/engine";
6171
+
6172
+ // src/services/heifRotation.ts
6173
+ function* boxes(buf, start, end) {
6174
+ let at = start;
6175
+ while (at + 8 <= end) {
6176
+ let size = buf.readUInt32BE(at);
6177
+ let header = 8;
6178
+ if (size === 1 && at + 16 <= end) {
6179
+ size = Number(buf.readBigUInt64BE(at + 8));
6180
+ header = 16;
6181
+ } else if (size === 0) {
6182
+ size = end - at;
6183
+ }
6184
+ if (size < header || at + size > end) return;
6185
+ yield { type: buf.toString("latin1", at + 4, at + 8), start: at + header, end: at + size };
6186
+ at += size;
6187
+ }
6188
+ }
6189
+ function child(buf, parent, type, skip = 0) {
6190
+ return [...boxes(buf, parent.start + skip, parent.end)].filter((box) => box.type === type);
6191
+ }
6192
+ function readHeifPrimaryRotation(buf) {
6193
+ try {
6194
+ const root = { type: "", start: 0, end: buf.length };
6195
+ const meta = child(buf, root, "meta")[0];
6196
+ const pitm = meta && child(buf, meta, "pitm", 4)[0];
6197
+ const iprp = meta && child(buf, meta, "iprp", 4)[0];
6198
+ const ipco = iprp && child(buf, iprp, "ipco")[0];
6199
+ if (!pitm || !ipco || !iprp) return 0;
6200
+ const primaryId = buf[pitm.start] === 0 ? buf.readUInt16BE(pitm.start + 4) : buf.readUInt32BE(pitm.start + 4);
6201
+ const props = [...boxes(buf, ipco.start, ipco.end)];
6202
+ for (const ipma of child(buf, iprp, "ipma")) {
6203
+ const version = buf[ipma.start];
6204
+ const wide = (buf[ipma.start + 3] & 1) === 1;
6205
+ let at = ipma.start + 8;
6206
+ for (let n = buf.readUInt32BE(ipma.start + 4); n > 0 && at < ipma.end; n--) {
6207
+ const id = version < 1 ? buf.readUInt16BE(at) : buf.readUInt32BE(at);
6208
+ at += version < 1 ? 2 : 4;
6209
+ const count = buf[at++];
6210
+ for (let i = 0; i < count; i++) {
6211
+ const index = wide ? buf.readUInt16BE(at) & 32767 : buf[at] & 127;
6212
+ at += wide ? 2 : 1;
6213
+ const prop = props[index - 1];
6214
+ if (id === primaryId && prop?.type === "irot")
6215
+ return (buf[prop.start] & 3) * 90;
6216
+ }
6217
+ }
6218
+ }
6219
+ } catch {
6220
+ }
6221
+ return 0;
6222
+ }
6147
6223
 
6148
6224
  // src/utils/semaphore.ts
6149
6225
  var Semaphore = class {
@@ -6158,10 +6234,10 @@ var Semaphore = class {
6158
6234
  this.active++;
6159
6235
  return () => this.release();
6160
6236
  }
6161
- return new Promise((resolve10) => {
6237
+ return new Promise((resolve11) => {
6162
6238
  this.queue.push(() => {
6163
6239
  this.active++;
6164
- resolve10(() => this.release());
6240
+ resolve11(() => this.release());
6165
6241
  });
6166
6242
  });
6167
6243
  }
@@ -6192,7 +6268,229 @@ async function withMediaProbeSlot(operation) {
6192
6268
  }
6193
6269
  }
6194
6270
 
6271
+ // src/services/heifImagePrep.ts
6272
+ var PREPARED_HEIF_SUBDIR = "_heif_images";
6273
+ var CACHE_SCHEMA2 = "hfheif-v1";
6274
+ var DEFAULT_TIMEOUT_MS = 12e4;
6275
+ var HEIF_IMAGE_CONVERSION_FAILED = "HEIF_IMAGE_CONVERSION_FAILED";
6276
+ var HEIF_TOOL_UNAVAILABLE = "HEIF_TOOL_UNAVAILABLE";
6277
+ var HEIF_BRANDS = /* @__PURE__ */ new Set(["heic", "heix", "hevc", "hevx", "heim", "heis", "mif1", "msf1"]);
6278
+ var AVIF_BRANDS = /* @__PURE__ */ new Set(["avif", "avis"]);
6279
+ var DATA_URL_RE = /^data:image\/(?:heic|heif)(?:-sequence)?;base64,(.*)$/is;
6280
+ var HeifImageConversionError = class extends Error {
6281
+ code = HEIF_IMAGE_CONVERSION_FAILED;
6282
+ owner = "user";
6283
+ retryable = false;
6284
+ constructor(label, reason) {
6285
+ super(
6286
+ `Could not convert HEIC/HEIF image ${label} to JPEG: ${reason}. Re-export the photo as JPEG or PNG and update its src.`
6287
+ );
6288
+ this.name = "HeifImageConversionError";
6289
+ }
6290
+ };
6291
+ var HeifToolUnavailableError = class extends Error {
6292
+ code = HEIF_TOOL_UNAVAILABLE;
6293
+ owner = "system";
6294
+ retryable = false;
6295
+ constructor(label, reason) {
6296
+ super(
6297
+ `Could not convert HEIC/HEIF image ${label}: ${reason}. Converting HEIC needs ffmpeg and ffprobe 7.0 or newer with the hevc decoder.`
6298
+ );
6299
+ this.name = "HeifToolUnavailableError";
6300
+ }
6301
+ };
6302
+ var TOOL_FAILURE_RE = /(?:ffmpeg|ffprobe) not found|ENOENT|Option not found|Unrecognized option|Unknown decoder|Decoder \S+ not found/i;
6303
+ function isHeifBytes(head) {
6304
+ const bytes = Buffer.from(head.buffer, head.byteOffset, head.byteLength);
6305
+ if (bytes.length < 16 || bytes.toString("ascii", 4, 8) !== "ftyp") return false;
6306
+ const boxEnd = Math.min(bytes.readUInt32BE(0), bytes.length);
6307
+ if (boxEnd < 16) return false;
6308
+ const brands = [bytes.toString("ascii", 8, 12)];
6309
+ for (let offset = 16; offset + 4 <= boxEnd; offset += 4) {
6310
+ brands.push(bytes.toString("ascii", offset, offset + 4));
6311
+ }
6312
+ return !brands.some((b) => AVIF_BRANDS.has(b)) && brands.some((b) => HEIF_BRANDS.has(b));
6313
+ }
6314
+ function readHead(path) {
6315
+ let fd;
6316
+ try {
6317
+ fd = openSync4(path, "r");
6318
+ const buf = Buffer.alloc(4096);
6319
+ return buf.subarray(0, readSync3(fd, buf, 0, buf.length, 0));
6320
+ } catch {
6321
+ return null;
6322
+ } finally {
6323
+ if (fd !== void 0) closeSync4(fd);
6324
+ }
6325
+ }
6326
+ async function probeJson(path, withGroups) {
6327
+ const stdout = await runFfprobe(
6328
+ path,
6329
+ withGroups ? ["-show_stream_groups", "-show_streams", "-of", "json"] : ["-show_streams", "-of", "json"]
6330
+ );
6331
+ return JSON.parse(stdout);
6332
+ }
6333
+ var ROTATION_FILTER = {
6334
+ 0: "",
6335
+ 90: "transpose=2",
6336
+ 180: "hflip,vflip",
6337
+ 270: "transpose=1"
6338
+ };
6339
+ function primaryGrid(probed) {
6340
+ const groups = probed.stream_groups ?? [];
6341
+ return (groups.find((g) => g.disposition?.default === 1) ?? groups[0])?.components?.[0];
6342
+ }
6343
+ async function convertHeifToJpeg(request) {
6344
+ const probed = await probeJson(request.inputPath, true);
6345
+ const grid = primaryGrid(probed);
6346
+ const rotation = readHeifPrimaryRotation(readFileSync6(request.inputPath));
6347
+ const filters = [];
6348
+ let inputs = [];
6349
+ let size = probed.streams?.[0];
6350
+ if (grid?.subcomponents?.length) {
6351
+ size = grid;
6352
+ const layout = grid.subcomponents.map(
6353
+ (t) => `${t.tile_horizontal_offset}_${t.tile_vertical_offset}`
6354
+ );
6355
+ inputs = grid.subcomponents.map((t) => `[0:${t.stream_index}]`);
6356
+ filters.push(
6357
+ `${inputs.join("")}xstack=inputs=${inputs.length}:layout=${layout.join("|")}`,
6358
+ `crop=${grid.width}:${grid.height}:${grid.horizontal_offset ?? 0}:${grid.vertical_offset ?? 0}`
6359
+ );
6360
+ }
6361
+ if (!size?.width || !size.height) throw new Error("no declared image size");
6362
+ if (rotation && grid) filters.push(ROTATION_FILTER[rotation]);
6363
+ const mapArgs = filters.length > 0 ? ["-filter_complex", `${filters.join(",")}[v]`, "-map", "[v]"] : [];
6364
+ const result = await runFfmpeg3(
6365
+ [
6366
+ "-hide_banner",
6367
+ "-loglevel",
6368
+ "error",
6369
+ "-i",
6370
+ request.inputPath,
6371
+ ...mapArgs,
6372
+ "-frames:v",
6373
+ "1",
6374
+ "-q:v",
6375
+ "2",
6376
+ "-f",
6377
+ "mjpeg",
6378
+ "-y",
6379
+ request.outputPath
6380
+ ],
6381
+ { timeout: request.timeoutMs ?? DEFAULT_TIMEOUT_MS }
6382
+ );
6383
+ if (!result.success) {
6384
+ throw result.error ?? new Error(`ffmpeg exited ${result.exitCode}: ${result.stderr.slice(-300)}`);
6385
+ }
6386
+ const out = (await probeJson(request.outputPath, false)).streams?.[0];
6387
+ const same = out?.width === size.width && out.height === size.height;
6388
+ const swapped = out?.width === size.height && out.height === size.width;
6389
+ if (!same && !swapped) {
6390
+ throw new Error(
6391
+ `wrong size: decoded ${out?.width}x${out?.height}, file declares ${size.width}x${size.height}`
6392
+ );
6393
+ }
6394
+ }
6395
+ function resolveImageSource(src, options) {
6396
+ const data = DATA_URL_RE.exec(src);
6397
+ if (data) {
6398
+ const bytes = Buffer.from(data[1], "base64");
6399
+ return isHeifBytes(bytes.subarray(0, 4096)) ? { path: null, bytes, label: '"inline image"' } : null;
6400
+ }
6401
+ const path = resolveLocalImageSourcePath(src, options);
6402
+ const head = path ? readHead(path) : null;
6403
+ if (!path || !head || !isHeifBytes(head)) return null;
6404
+ return { path, bytes: readFileSync6(path), label: `"${splitUrlSuffix(src).basePath}"` };
6405
+ }
6406
+ function isUsableFile2(path) {
6407
+ try {
6408
+ return statSync6(path).size > 0;
6409
+ } catch {
6410
+ return false;
6411
+ }
6412
+ }
6413
+ async function ensurePreparedJpeg(source, cachePath, outputPath, options) {
6414
+ mkdirSync6(dirname6(cachePath), { recursive: true });
6415
+ mkdirSync6(dirname6(outputPath), { recursive: true });
6416
+ if (!isUsableFile2(cachePath)) {
6417
+ const tmpPath = `${cachePath}.tmp-${process.pid}-${Date.now()}`;
6418
+ const inlinePath = `${tmpPath}.heic`;
6419
+ const inputPath = source.path ?? inlinePath;
6420
+ try {
6421
+ if (!source.path) writeFileSync4(inlinePath, source.bytes);
6422
+ await (options.transcode ?? convertHeifToJpeg)({
6423
+ inputPath,
6424
+ outputPath: tmpPath,
6425
+ timeoutMs: options.timeoutMs
6426
+ });
6427
+ if (!isUsableFile2(tmpPath)) throw new Error("conversion produced an empty file");
6428
+ renameWithRetry4(tmpPath, cachePath);
6429
+ } catch (error) {
6430
+ const reason = error instanceof Error ? error.message : String(error);
6431
+ throw TOOL_FAILURE_RE.test(reason) ? new HeifToolUnavailableError(source.label, reason) : new HeifImageConversionError(source.label, reason);
6432
+ } finally {
6433
+ rmSync8(tmpPath, { force: true });
6434
+ rmSync8(inlinePath, { force: true });
6435
+ }
6436
+ }
6437
+ if (resolve6(outputPath) !== resolve6(cachePath) && !isUsableFile2(outputPath)) {
6438
+ copyFileSync3(cachePath, outputPath);
6439
+ }
6440
+ }
6441
+ async function prepareHeifImageInputs(html, options) {
6442
+ const outputDir = options.outputDir ?? join14(options.downloadDir, PREPARED_HEIF_SUBDIR);
6443
+ const outputSrcPrefix = normalizeRelPath(options.outputSrcPrefix ?? PREPARED_HEIF_SUBDIR);
6444
+ const cacheDir = options.cacheDir ?? outputDir;
6445
+ const preparedAssets = /* @__PURE__ */ new Map();
6446
+ const preparedImages = [];
6447
+ if (!/<img\b/i.test(html)) return { html, preparedAssets, preparedImages };
6448
+ const { document: document2 } = parseHTML4(html);
6449
+ const found = [];
6450
+ for (const img of Array.from(document2.querySelectorAll("img[src]"))) {
6451
+ const src = (img.getAttribute("src") || "").trim();
6452
+ const source = src ? resolveImageSource(src, options) : null;
6453
+ if (!source) continue;
6454
+ const hash = createHash5("sha256").update(CACHE_SCHEMA2).update("\0").update(source.bytes).digest("hex");
6455
+ found.push({ img, src, source, filename: `${CACHE_SCHEMA2}-${hash.slice(0, 24)}.jpg` });
6456
+ }
6457
+ if (found.length === 0) return { html, preparedAssets, preparedImages };
6458
+ const unique = new Map(found.map((item) => [item.filename, item.source]));
6459
+ const failed = /* @__PURE__ */ new Set();
6460
+ await Promise.all(
6461
+ [...unique].map(
6462
+ ([filename, source]) => withMediaProbeSlot(
6463
+ () => ensurePreparedJpeg(source, join14(cacheDir, filename), join14(outputDir, filename), options)
6464
+ ).catch((error) => {
6465
+ if (!options.onFailure) throw error;
6466
+ failed.add(filename);
6467
+ options.onFailure(error);
6468
+ })
6469
+ )
6470
+ );
6471
+ for (const { img, src, filename } of found) {
6472
+ if (failed.has(filename)) continue;
6473
+ const outputSrc = `${outputSrcPrefix}/${filename}`;
6474
+ img.setAttribute("src", outputSrc);
6475
+ preparedAssets.set(outputSrc, join14(outputDir, filename));
6476
+ preparedImages.push({ sourceSrc: src, outputSrc, outputPath: join14(outputDir, filename) });
6477
+ }
6478
+ return { html: document2.toString(), preparedAssets, preparedImages };
6479
+ }
6480
+
6481
+ // src/services/htmlCompiler.ts
6482
+ import { createStudioPositionSeekReapplyScript } from "@hyperframes/studio-server/manual-edits-render-script";
6483
+ import { getPositionEditsRenderScript } from "@hyperframes/core/runtime/position-edits-render";
6484
+
6195
6485
  // src/services/assetMediaType.ts
6486
+ import { existsSync as existsSync9 } from "node:fs";
6487
+ import {
6488
+ fingerprintElementId,
6489
+ isNotMediaPayload,
6490
+ NotMediaPayloadError,
6491
+ probeMediaProfile,
6492
+ resolveProjectRelativeSrc as resolveProjectRelativeSrc2
6493
+ } from "@hyperframes/engine";
6196
6494
  var ASSET_MEDIA_TYPE_MISMATCH = "ASSET_MEDIA_TYPE_MISMATCH";
6197
6495
  var AssetMediaTypeMismatchError = class extends Error {
6198
6496
  code = ASSET_MEDIA_TYPE_MISMATCH;
@@ -6302,7 +6600,7 @@ function logRemoteDownloadTelemetry(event) {
6302
6600
  }
6303
6601
  var INFERRED_MEDIA_DURATION_ATTR = "data-hf-inferred-duration";
6304
6602
  function parseSubCompHtmlForValidity(html) {
6305
- return parseHTML4(html).document;
6603
+ return parseHTML5(html).document;
6306
6604
  }
6307
6605
  function endsWithSourceUrl(code) {
6308
6606
  const trimmed = code.trimEnd();
@@ -6310,7 +6608,7 @@ function endsWithSourceUrl(code) {
6310
6608
  return lastLine.startsWith("//# sourceURL=") || lastLine.startsWith("//@ sourceURL=");
6311
6609
  }
6312
6610
  function prepareCompositionScripts(html) {
6313
- const { document: document2 } = parseHTML4(html);
6611
+ const { document: document2 } = parseHTML5(html);
6314
6612
  for (const el of document2.querySelectorAll("script:not([src])")) {
6315
6613
  const isModule = (el.getAttribute("type") || "").trim().toLowerCase() === "module";
6316
6614
  const runsAsScript = isModule || isJavaScriptType(el);
@@ -6347,14 +6645,14 @@ Check that each file referenced by data-composition-src contains valid HTML with
6347
6645
  }
6348
6646
  };
6349
6647
  function assertSubCompositionsUsable(html, projectDir, visited = /* @__PURE__ */ new Set()) {
6350
- const { document: document2 } = parseHTML4(html);
6648
+ const { document: document2 } = parseHTML5(html);
6351
6649
  const hosts = [...document2.querySelectorAll("[data-composition-src]")];
6352
6650
  const problems = [];
6353
6651
  for (const el of hosts) {
6354
6652
  const srcPath = el.getAttribute("data-composition-src");
6355
6653
  if (!srcPath) continue;
6356
6654
  if (isUnresolvedAssetPlaceholder(srcPath)) continue;
6357
- const filePath = resolve6(projectDir, srcPath);
6655
+ const filePath = resolve7(projectDir, srcPath);
6358
6656
  if (visited.has(filePath)) continue;
6359
6657
  const read = readProjectFile(filePath);
6360
6658
  if (read.kind === "missing") {
@@ -6410,7 +6708,7 @@ function runtimeRoot(document2) {
6410
6708
  }
6411
6709
  function detectRenderModeHints(html, pageMotionBlur) {
6412
6710
  const reasons = [];
6413
- const { document: document2 } = parseHTML4(html);
6711
+ const { document: document2 } = parseHTML5(html);
6414
6712
  if (document2.querySelector("canvas[layoutsubtree]")) {
6415
6713
  reasons.push({
6416
6714
  code: "htmlInCanvas",
@@ -6452,7 +6750,7 @@ function detectThreeDTransformUsage(html) {
6452
6750
  }
6453
6751
  var BACKGROUND_IMAGE_DECL_PATTERN = /(?:^|;|\{)\s*background(?:-image)?\s*:[^;}]*(?:\bgradient\s*\(|url\s*\()/i;
6454
6752
  function detectAncestorBackgroundImage(html) {
6455
- const { document: document2 } = parseHTML4(html);
6753
+ const { document: document2 } = parseHTML5(html);
6456
6754
  const root = document2.querySelector("[data-composition-id]");
6457
6755
  if (!root) return false;
6458
6756
  const ancestors = [];
@@ -6499,7 +6797,7 @@ function detectShaderTransitionUsage(html) {
6499
6797
  async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, downloadDir, tagName, elementIdentity, log, urlByAbsentLocalPath) {
6500
6798
  let filePath = src;
6501
6799
  if (isHttpUrl(src)) {
6502
- if (!existsSync10(downloadDir)) mkdirSync6(downloadDir, { recursive: true });
6800
+ if (!existsSync10(downloadDir)) mkdirSync7(downloadDir, { recursive: true });
6503
6801
  try {
6504
6802
  filePath = await downloadToTemp(src, downloadDir, void 0, void 0, void 0, {
6505
6803
  onTelemetry: logRemoteDownloadTelemetry
@@ -6509,7 +6807,7 @@ async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, down
6509
6807
  }
6510
6808
  } else {
6511
6809
  filePath = decodedUrlPath(src);
6512
- if (!filePath.startsWith("/")) filePath = join14(baseDir, filePath);
6810
+ if (!filePath.startsWith("/")) filePath = join15(baseDir, filePath);
6513
6811
  filePath = urlByAbsentLocalPath?.get(filePath) ?? filePath;
6514
6812
  }
6515
6813
  if (!isHttpUrl(filePath) && !existsSync10(filePath)) {
@@ -6566,7 +6864,7 @@ async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, down
6566
6864
  function markInferredVariableMediaDurations(html, unresolvedMedia) {
6567
6865
  const unresolvedIds = new Set(unresolvedMedia.map((element) => element.id));
6568
6866
  if (unresolvedIds.size === 0) return html;
6569
- const { document: document2 } = parseHTML4(html);
6867
+ const { document: document2 } = parseHTML5(html);
6570
6868
  let changed = false;
6571
6869
  for (const element of document2.querySelectorAll("video[data-var-src], audio[data-var-src]")) {
6572
6870
  if (!unresolvedIds.has(element.id)) continue;
@@ -6637,13 +6935,13 @@ async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocal
6637
6935
  }
6638
6936
  async function parseSubCompositions(html, projectDir, downloadDir, visited = /* @__PURE__ */ new Set(), urlByAbsentLocalPath) {
6639
6937
  const subCompositions = /* @__PURE__ */ new Map();
6640
- const { document: document2 } = parseHTML4(html);
6938
+ const { document: document2 } = parseHTML5(html);
6641
6939
  const compEls = document2.querySelectorAll("[data-composition-src]");
6642
6940
  const workItems = [];
6643
6941
  for (const el of compEls) {
6644
6942
  const srcPath = el.getAttribute("data-composition-src");
6645
6943
  if (!srcPath) continue;
6646
- const filePath = resolve6(projectDir, srcPath);
6944
+ const filePath = resolve7(projectDir, srcPath);
6647
6945
  if (visited.has(filePath)) {
6648
6946
  continue;
6649
6947
  }
@@ -6660,7 +6958,7 @@ async function parseSubCompositions(html, projectDir, downloadDir, visited = /*
6660
6958
  workItems.map(async (item) => {
6661
6959
  const { html: compiledSub } = await compileHtmlFile(
6662
6960
  rewriteUnresolvableGsapToCdn(item.rawSubHtml, projectDir, item.srcPath),
6663
- dirname6(item.filePath),
6961
+ dirname7(item.filePath),
6664
6962
  downloadDir,
6665
6963
  void 0,
6666
6964
  urlByAbsentLocalPath
@@ -6688,7 +6986,7 @@ async function parseSubCompositions(html, projectDir, downloadDir, visited = /*
6688
6986
  return { subCompositions };
6689
6987
  }
6690
6988
  function promoteCssImportsToLinkTags(html) {
6691
- const { document: document2 } = parseHTML4(html);
6989
+ const { document: document2 } = parseHTML5(html);
6692
6990
  const head = document2.querySelector("head");
6693
6991
  if (!head) return html;
6694
6992
  const importRe = /@import\s+url\(\s*['"]?([^'")\s]+)['"]?\s*\)\s*;?/gi;
@@ -6757,10 +7055,15 @@ var ProducerHostIdentityMap = class extends Map {
6757
7055
  }
6758
7056
  };
6759
7057
  function coalesceHeadStylesAndBodyScripts(html) {
6760
- const { document: document2 } = parseHTML4(html);
7058
+ const { document: document2 } = parseHTML5(html);
6761
7059
  const head = document2.querySelector("head");
6762
7060
  const body = document2.querySelector("body");
6763
7061
  if (!head && !body) return html;
7062
+ const liveStyles = [...document2.querySelectorAll("style")].filter(isAlwaysAppliedStyle);
7063
+ const dedupedStyles = dedupeFontFaceRules(liveStyles.map((el) => el.textContent || ""));
7064
+ liveStyles.forEach((el, i) => {
7065
+ el.textContent = dedupedStyles[i] ?? "";
7066
+ });
6764
7067
  const styleEls = head ? Array.from(head.querySelectorAll("style")) : [];
6765
7068
  const importRe = /@import\s+url\([^)]*\)\s*;|@import\s+["'][^"']+["']\s*;/gi;
6766
7069
  for (const run of styleEls.length > 1 ? headStyleRuns(styleEls) : []) {
@@ -6803,7 +7106,7 @@ function coalesceHeadStylesAndBodyScripts(html) {
6803
7106
  return document2.toString();
6804
7107
  }
6805
7108
  function inlineSubCompositions(html, subCompositions, projectDir, variableOverrides = {}) {
6806
- const { document: document2 } = parseHTML4(html);
7109
+ const { document: document2 } = parseHTML5(html);
6807
7110
  const head = document2.querySelector("head");
6808
7111
  const body = document2.querySelector("body");
6809
7112
  const hosts = Array.from(document2.querySelectorAll("[data-composition-src]"));
@@ -6832,17 +7135,17 @@ function inlineSubCompositions(html, subCompositions, projectDir, variableOverri
6832
7135
  resolveHtml: (srcPath) => {
6833
7136
  let compHtml = subCompositions.get(srcPath) || null;
6834
7137
  if (!compHtml) {
6835
- const read = readProjectFile(resolve6(projectDir, srcPath));
7138
+ const read = readProjectFile(resolve7(projectDir, srcPath));
6836
7139
  if (read.kind === "file") {
6837
7140
  compHtml = rewriteUnresolvableGsapToCdn(read.text, projectDir, srcPath);
6838
7141
  }
6839
7142
  }
6840
7143
  return compHtml;
6841
7144
  },
6842
- parseHtml: (htmlStr) => parseHTML4(htmlStr).document,
7145
+ parseHtml: (htmlStr) => parseHTML5(htmlStr).document,
6843
7146
  // Mirrors the preview bundler: a sub-composition's SIBLING assets resolve
6844
7147
  // against its own directory, project-root refs stay as authored.
6845
- assetExists: (path) => existsSync10(resolve6(projectDir, path)),
7148
+ assetExists: (path) => existsSync10(resolve7(projectDir, path)),
6846
7149
  // Preserve the authored root wrapper as a child of the host, matching
6847
7150
  // the preview bundler's shape (htmlBundler.ts's prepareFlattenedInnerRoot,
6848
7151
  // which the runtime compositionLoader mirrors with its own copy for the
@@ -6943,7 +7246,7 @@ ${html}
6943
7246
  </html>`;
6944
7247
  }
6945
7248
  function injectTextRenderingRule(html) {
6946
- const { document: document2 } = parseHTML4(html);
7249
+ const { document: document2 } = parseHTML5(html);
6947
7250
  const head = document2.querySelector("head");
6948
7251
  if (!head) return html;
6949
7252
  if (document2.querySelector("style[data-hyperframes-text-rendering]")) {
@@ -6967,7 +7270,7 @@ function matchesScriptIntegrity(bytes, metadata) {
6967
7270
  (alg) => hashes.some((hash) => hash[1]?.toLowerCase() === alg)
6968
7271
  );
6969
7272
  if (!strongest) return true;
6970
- const actual = createHash5(strongest).update(bytes).digest();
7273
+ const actual = createHash6(strongest).update(bytes).digest();
6971
7274
  return hashes.some(
6972
7275
  (hash) => hash[1]?.toLowerCase() === strongest && actual.equals(Buffer.from(hash[2], "base64"))
6973
7276
  );
@@ -6975,7 +7278,7 @@ function matchesScriptIntegrity(bytes, metadata) {
6975
7278
  async function inlineExternalScripts(html) {
6976
7279
  const fullHtml = ensureFullDocument(html);
6977
7280
  const wrappedFragment = fullHtml !== html;
6978
- const { document: document2 } = parseHTML4(fullHtml);
7281
+ const { document: document2 } = parseHTML5(fullHtml);
6979
7282
  const scripts = document2.querySelectorAll("script[src]");
6980
7283
  const externalScripts = [];
6981
7284
  for (const el of scripts) {
@@ -7024,12 +7327,12 @@ ${safeText}
7024
7327
  return wrappedFragment ? document2.body.innerHTML || "" : document2.toString();
7025
7328
  }
7026
7329
  function collectExternalAssets(html, projectDir) {
7027
- const absProjectDir = resolve6(projectDir);
7330
+ const absProjectDir = resolve7(projectDir);
7028
7331
  const externalAssets = /* @__PURE__ */ new Map();
7029
7332
  function processPath(rawPath) {
7030
7333
  const trimmed = rawPath.trim();
7031
7334
  if (isNonRelativeUrl(trimmed)) return null;
7032
- const absPath = resolve6(absProjectDir, trimmed);
7335
+ const absPath = resolve7(absProjectDir, trimmed);
7033
7336
  if (isPathInside2(absPath, absProjectDir)) {
7034
7337
  return null;
7035
7338
  }
@@ -7038,7 +7341,7 @@ function collectExternalAssets(html, projectDir) {
7038
7341
  externalAssets.set(safeKey, absPath);
7039
7342
  return safeKey;
7040
7343
  }
7041
- const { document: document2 } = parseHTML4(html);
7344
+ const { document: document2 } = parseHTML5(html);
7042
7345
  for (const el of document2.querySelectorAll("[src], [href]")) {
7043
7346
  for (const attr of ["src", "href"]) {
7044
7347
  const val = (el.getAttribute(attr) || "").trim();
@@ -7083,7 +7386,7 @@ var REMOTE_SOURCE_TAG_RE = /<source\b[^>]*?\bsrc\s*=\s*["'](https?:\/\/[^"']+)["
7083
7386
  var REMOTE_IMG_TAG_RE = /<img\b[^>]*?(?<![\w-])src\s*=\s*["'](https?:\/\/[^"']+)["'][^>]*>/gi;
7084
7387
  async function downloadAndRewriteUrls(urlSet, html, remoteDir, warnLabel, logLabel, extraRewrite) {
7085
7388
  if (urlSet.size === 0) return { html, remoteMediaAssets: /* @__PURE__ */ new Map() };
7086
- if (!existsSync10(remoteDir)) mkdirSync6(remoteDir, { recursive: true });
7389
+ if (!existsSync10(remoteDir)) mkdirSync7(remoteDir, { recursive: true });
7087
7390
  const urlToLocal = /* @__PURE__ */ new Map();
7088
7391
  await Promise.all(
7089
7392
  [...urlSet].map(async (url) => {
@@ -7130,7 +7433,7 @@ async function localizeRemoteMediaSources(html, downloadDir) {
7130
7433
  return downloadAndRewriteUrls(
7131
7434
  urlSet,
7132
7435
  html,
7133
- join14(downloadDir, REMOTE_MEDIA_SUBDIR),
7436
+ join15(downloadDir, REMOTE_MEDIA_SUBDIR),
7134
7437
  "Remote media download failed",
7135
7438
  "Localized remote media source(s)"
7136
7439
  );
@@ -7145,7 +7448,7 @@ async function localizeRemoteImageSources(html, downloadDir) {
7145
7448
  return downloadAndRewriteUrls(
7146
7449
  urlSet,
7147
7450
  html,
7148
- join14(downloadDir, REMOTE_MEDIA_SUBDIR),
7451
+ join15(downloadDir, REMOTE_MEDIA_SUBDIR),
7149
7452
  "Remote image download failed",
7150
7453
  "Localized remote image source(s)"
7151
7454
  );
@@ -7161,7 +7464,7 @@ async function localizeRemoteBackgroundImages(html, downloadDir) {
7161
7464
  return downloadAndRewriteUrls(
7162
7465
  urlSet,
7163
7466
  html,
7164
- join14(downloadDir, REMOTE_MEDIA_SUBDIR),
7467
+ join15(downloadDir, REMOTE_MEDIA_SUBDIR),
7165
7468
  "Remote background-image download failed",
7166
7469
  "Localized remote background-image(s)",
7167
7470
  // Quoted url('..')/url("..") are rewritten by downloadAndRewriteUrls' default
@@ -7296,7 +7599,7 @@ async function localizeRemoteFontFaces(html, downloadDir) {
7296
7599
  return downloadAndRewriteUrls(
7297
7600
  urlSet,
7298
7601
  processed,
7299
- join14(downloadDir, REMOTE_MEDIA_SUBDIR),
7602
+ join15(downloadDir, REMOTE_MEDIA_SUBDIR),
7300
7603
  "Remote font download failed",
7301
7604
  "Localized remote font face(s)",
7302
7605
  (h, url, relPath) => h.replaceAll(`url(${url})`, `url("${relPath}")`)
@@ -7341,7 +7644,7 @@ async function embedLocalFontFaces(html, projectDir) {
7341
7644
  while ((urlMatch = urlRe.exec(block)) !== null) {
7342
7645
  const localPath = urlMatch[1];
7343
7646
  if (!localPath || embeddedPaths.has(localPath)) continue;
7344
- const absPath = localPath.startsWith("/") ? localPath : resolve6(projectDir, localPath);
7647
+ const absPath = localPath.startsWith("/") ? localPath : resolve7(projectDir, localPath);
7345
7648
  if (!isPathInside2(absPath, projectDir)) continue;
7346
7649
  const ext = absPath.match(/\.(woff2?|ttf|otf|ttc)$/i)?.[1]?.toLowerCase() ?? "ttf";
7347
7650
  try {
@@ -7390,7 +7693,7 @@ function renderGsapDist() {
7390
7693
  }
7391
7694
  function rewriteUnresolvableGsapToCdn(html, projectDir, compSrcPath) {
7392
7695
  let gsapDist;
7393
- const exists = (path) => existsSync10(resolve6(projectDir, path));
7696
+ const exists = (path) => existsSync10(resolve7(projectDir, path));
7394
7697
  return html.replace(
7395
7698
  /(<script\b[^>]*\bsrc=["'])([^"']*gsap[^"']*\/dist\/([^"']+))(["'][^>]*>)/gi,
7396
7699
  (full, prefix, src, file, suffix) => {
@@ -7406,8 +7709,8 @@ function rebaseDirectEntryAssetPaths(html, projectDir, htmlPath) {
7406
7709
  if (!isPathInside2(htmlPath, projectDir)) return html;
7407
7710
  const entryPath = relative2(projectDir, htmlPath).replace(/\\/g, "/");
7408
7711
  if (!entryPath.includes("/")) return html;
7409
- const { document: document2 } = parseHTML4(html);
7410
- const assetExists = (path) => existsSync10(resolve6(projectDir, path));
7712
+ const { document: document2 } = parseHTML5(html);
7713
+ const assetExists = (path) => existsSync10(resolve7(projectDir, path));
7411
7714
  rewriteAssetPaths(
7412
7715
  document2.querySelectorAll("[src], [href]"),
7413
7716
  entryPath,
@@ -7429,7 +7732,7 @@ function rebaseDirectEntryAssetPaths(html, projectDir, htmlPath) {
7429
7732
  }
7430
7733
  async function compileForRender(projectDir, htmlPath, downloadDir, options = {}) {
7431
7734
  const entryHtml = rebaseDirectEntryAssetPaths(
7432
- readFileSync6(htmlPath, "utf-8"),
7735
+ readFileSync7(htmlPath, "utf-8"),
7433
7736
  projectDir,
7434
7737
  htmlPath
7435
7738
  );
@@ -7499,12 +7802,26 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
7499
7802
  const { html: htmlWithLocalImages, remoteMediaAssets: remoteImageAssets } = await localizeRemoteImageSources(htmlWithLocalMedia, downloadDir);
7500
7803
  const { html: htmlWithLocalBg, remoteMediaAssets: remoteBgAssets } = await localizeRemoteBackgroundImages(htmlWithLocalImages, downloadDir);
7501
7804
  const { html: htmlWithLocalizedFonts, remoteMediaAssets: remoteFontAssets } = await localizeRemoteFontFaces(htmlWithLocalBg, downloadDir);
7805
+ const {
7806
+ html: htmlWithJpegs,
7807
+ preparedAssets: preparedHeifAssets,
7808
+ preparedImages: preparedHeifImages
7809
+ } = await prepareHeifImageInputs(htmlWithLocalizedFonts, {
7810
+ projectDir,
7811
+ downloadDir,
7812
+ cacheDir: options.animatedGifCacheDir,
7813
+ sourceAssets: remoteImageAssets,
7814
+ timeoutMs: options.ffmpegProcessTimeout
7815
+ });
7816
+ if (preparedHeifImages.length > 0) {
7817
+ defaultLogger.info(`[Compiler] Converted ${preparedHeifImages.length} HEIC image(s) to JPEG`);
7818
+ }
7502
7819
  const gifSourceAssets = new Map(remoteImageAssets);
7503
7820
  const {
7504
7821
  html: htmlWithPreparedGifs,
7505
7822
  preparedAssets: preparedGifAssets,
7506
7823
  preparedGifs
7507
- } = await prepareAnimatedGifInputs(htmlWithLocalizedFonts, {
7824
+ } = await prepareAnimatedGifInputs(htmlWithJpegs, {
7508
7825
  projectDir,
7509
7826
  downloadDir,
7510
7827
  cacheDir: options.animatedGifCacheDir,
@@ -7531,11 +7848,14 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
7531
7848
  for (const [relPath, absPath] of preparedGifAssets) {
7532
7849
  externalAssets.set(relPath, absPath);
7533
7850
  }
7851
+ for (const [relPath, absPath] of preparedHeifAssets) {
7852
+ externalAssets.set(relPath, absPath);
7853
+ }
7534
7854
  const { videos, audios, images, droppedMediaIds } = collectRenderMedia(html);
7535
7855
  const masterFxChain = parseMasterFxChain(html);
7536
7856
  for (const video of videos) {
7537
7857
  if (isHttpUrl(video.src)) continue;
7538
- const videoPath = resolve6(projectDir, video.src);
7858
+ const videoPath = resolve7(projectDir, video.src);
7539
7859
  const reencode = `ffmpeg -i "${video.src}" -c:v libx264 -r 30 -g 30 -keyint_min 30 -movflags +faststart -c:a copy output.mp4`;
7540
7860
  (async () => {
7541
7861
  const metadata = await withMediaProbeSlot(() => extractMediaMetadata(videoPath));
@@ -7555,7 +7875,7 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
7555
7875
  })().catch(() => {
7556
7876
  });
7557
7877
  }
7558
- const { document: document2 } = parseHTML4(html);
7878
+ const { document: document2 } = parseHTML5(html);
7559
7879
  const rootEl = document2.querySelector("[data-composition-id]");
7560
7880
  const width = rootEl ? parseInt(rootEl.getAttribute("data-width") || "1080", 10) : 1080;
7561
7881
  const height = rootEl ? parseInt(rootEl.getAttribute("data-height") || "1920", 10) : 1920;
@@ -7962,7 +8282,7 @@ async function runCompileStage(input) {
7962
8282
  abortSignal
7963
8283
  } = input;
7964
8284
  const compileStart = Date.now();
7965
- const compiled = await compileForRender(projectDir, htmlPath, join15(workDir, "downloads"), {
8285
+ const compiled = await compileForRender(projectDir, htmlPath, join16(workDir, "downloads"), {
7966
8286
  log,
7967
8287
  failClosedFontFetch: failClosedFontFetch === true,
7968
8288
  allowSystemFontCapture,
@@ -7970,7 +8290,7 @@ async function runCompileStage(input) {
7970
8290
  variables: input.variables,
7971
8291
  urlByAbsentLocalPath: input.urlByAbsentLocalPath,
7972
8292
  pageMotionBlur: input.pageMotionBlur,
7973
- animatedGifCacheDir: cfg.extractCacheDir ? join15(cfg.extractCacheDir, "animated-gif") : void 0,
8293
+ animatedGifCacheDir: cfg.extractCacheDir ? join16(cfg.extractCacheDir, "animated-gif") : void 0,
7974
8294
  ffmpegProcessTimeout: cfg.ffmpegProcessTimeout
7975
8295
  });
7976
8296
  assertNotAborted();
@@ -8026,7 +8346,7 @@ async function runCompileStage(input) {
8026
8346
  };
8027
8347
  await preflightCompositionAssetMediaTypes({
8028
8348
  projectDir,
8029
- compiledDir: join15(workDir, "compiled"),
8349
+ compiledDir: join16(workDir, "compiled"),
8030
8350
  composition,
8031
8351
  signal: abortSignal
8032
8352
  });
@@ -8071,8 +8391,8 @@ async function runCompileStage(input) {
8071
8391
  }
8072
8392
 
8073
8393
  // src/services/render/stages/probeStage.ts
8074
- import { join as join16 } from "node:path";
8075
- import { parseHTML as parseHTML5 } from "linkedom";
8394
+ import { join as join17 } from "node:path";
8395
+ import { parseHTML as parseHTML6 } from "linkedom";
8076
8396
  import {
8077
8397
  closeCaptureSession as closeCaptureSession4,
8078
8398
  compositionRequiresWebGpu,
@@ -8119,7 +8439,7 @@ function durationToFrameCount(duration, fps) {
8119
8439
  }
8120
8440
  function hasScriptedAudioVolumeAutomation(html, audioCount) {
8121
8441
  if (audioCount <= 0) return false;
8122
- const { document: document2 } = parseHTML5(html);
8442
+ const { document: document2 } = parseHTML6(html);
8123
8443
  const scriptBodies = [...document2.querySelectorAll("script")].map((script) => script.textContent ?? "").join("\n");
8124
8444
  if (!scriptBodies) return false;
8125
8445
  return /\.\s*volume\s*=/i.test(scriptBodies) || /\b(?:gsap|tl|timeline|tween)\s*\.\s*(?:to|fromTo|set)\s*\([\s\S]{0,2000}\bvolume\s*:/i.test(
@@ -8127,12 +8447,12 @@ function hasScriptedAudioVolumeAutomation(html, audioCount) {
8127
8447
  );
8128
8448
  }
8129
8449
  function hasAutoStartVideos(html) {
8130
- const { document: document2 } = parseHTML5(html);
8450
+ const { document: document2 } = parseHTML6(html);
8131
8451
  return document2.querySelector("video[data-hf-auto-start]") !== null;
8132
8452
  }
8133
8453
  function hasVariableBoundMedia(html, variables) {
8134
8454
  if (!variables || Object.keys(variables).length === 0) return false;
8135
- const { document: document2 } = parseHTML5(html);
8455
+ const { document: document2 } = parseHTML6(html);
8136
8456
  return Array.from(
8137
8457
  document2.querySelectorAll(
8138
8458
  "img[data-var-src], audio[data-var-src], video[data-var-src], source[data-var-src]"
@@ -8143,7 +8463,7 @@ function hasVariableBoundMedia(html, variables) {
8143
8463
  });
8144
8464
  }
8145
8465
  function authoredMediaStarts(html) {
8146
- const { document: document2 } = parseHTML5(html);
8466
+ const { document: document2 } = parseHTML6(html);
8147
8467
  const starts = /* @__PURE__ */ new Map();
8148
8468
  for (const media of document2.querySelectorAll("video, audio")) {
8149
8469
  const id = media.getAttribute(MEDIA_RENDER_ID_ATTR2) || media.getAttribute("id");
@@ -8180,7 +8500,7 @@ function reconcileBrowserMediaEnd(existingEnd, projectedEnd, sourceChanged, dura
8180
8500
  return existingEnd <= 0 ? projectedEnd : Math.min(existingEnd, projectedEnd);
8181
8501
  }
8182
8502
  function hasRuntimeMediaChanges(html) {
8183
- const { document: document2 } = parseHTML5(html);
8503
+ const { document: document2 } = parseHTML6(html);
8184
8504
  const scriptBodies = [...document2.querySelectorAll("script")].map(
8185
8505
  (script) => script.textContent ?? ""
8186
8506
  );
@@ -8263,7 +8583,7 @@ async function runProbeStage(input) {
8263
8583
  });
8264
8584
  fileServer = await createFileServer({
8265
8585
  projectDir,
8266
- compiledDir: join16(workDir, "compiled"),
8586
+ compiledDir: join17(workDir, "compiled"),
8267
8587
  port: 0,
8268
8588
  preHeadScripts: [VIRTUAL_TIME_SHIM],
8269
8589
  fps: job.config.fps
@@ -8292,7 +8612,7 @@ async function runProbeStage(input) {
8292
8612
  log.info("Creating capture session...", { attempt, maxAttempts: PROBE_MAX_ATTEMPTS });
8293
8613
  probeSession = await createCaptureSession4(
8294
8614
  fileServer.url,
8295
- join16(workDir, "probe"),
8615
+ join17(workDir, "probe"),
8296
8616
  captureOpts,
8297
8617
  null,
8298
8618
  probeCfg
@@ -8377,7 +8697,7 @@ async function runProbeStage(input) {
8377
8697
  });
8378
8698
  probeSession = await createCaptureSession4(
8379
8699
  fileServer.url,
8380
- join16(workDir, "probe-screenshot"),
8700
+ join17(workDir, "probe-screenshot"),
8381
8701
  captureOpts,
8382
8702
  null,
8383
8703
  { ...probeCfg, forceScreenshot: true }
@@ -8413,7 +8733,7 @@ async function runProbeStage(input) {
8413
8733
  compiled,
8414
8734
  resolutions,
8415
8735
  projectDir,
8416
- join16(workDir, "downloads")
8736
+ join17(workDir, "downloads")
8417
8737
  );
8418
8738
  assertNotAborted();
8419
8739
  composition.videos = compiled.videos;
@@ -8604,7 +8924,7 @@ async function runProbeStage(input) {
8604
8924
  }
8605
8925
  await preflightCompositionAssetMediaTypes({
8606
8926
  projectDir,
8607
- compiledDir: join16(workDir, "compiled"),
8927
+ compiledDir: join17(workDir, "compiled"),
8608
8928
  composition,
8609
8929
  signal: abortSignal
8610
8930
  });
@@ -8738,7 +9058,7 @@ function validateRenderDuration(input) {
8738
9058
 
8739
9059
  // src/services/render/stages/extractVideosStage.ts
8740
9060
  import { existsSync as existsSync11 } from "node:fs";
8741
- import { join as join17 } from "node:path";
9061
+ import { join as join18 } from "node:path";
8742
9062
  import {
8743
9063
  classifyVideoExtractionError,
8744
9064
  createFrameLookupTable,
@@ -9053,14 +9373,14 @@ async function runExtractVideosStage(input) {
9053
9373
  // because short boundary counts can differ by one frame.
9054
9374
  {
9055
9375
  fps: job.config.fps,
9056
- outputDir: join17(compiledDir, "__hyperframes_video_frames"),
9376
+ outputDir: join18(compiledDir, "__hyperframes_video_frames"),
9057
9377
  format: job.config.videoFrameFormat ?? "auto",
9058
9378
  toneMapHdrToSdr: job.config.hdrMode === "force-sdr",
9059
9379
  timelineEnd: composition.duration,
9060
9380
  maxTransientRetries: extractionPolicy.maxTransientRetries,
9061
9381
  collectProbeFailures: extractionPolicy.failureMode === "enforce",
9062
9382
  deferRangeExtraction,
9063
- contentKeyedDirs: [join17(compiledDir, REMOTE_MEDIA_SUBDIR)],
9383
+ contentKeyedDirs: [join18(compiledDir, REMOTE_MEDIA_SUBDIR)],
9064
9384
  urlByAbsentLocalPath
9065
9385
  },
9066
9386
  abortSignal,
@@ -9127,8 +9447,8 @@ function appendAutoDetectedVideoAudio(composition, extracted) {
9127
9447
  }
9128
9448
 
9129
9449
  // src/services/render/stages/audioStage.ts
9130
- import { join as join19 } from "node:path";
9131
- import { rmSync as rmSync9 } from "node:fs";
9450
+ import { join as join20 } from "node:path";
9451
+ import { rmSync as rmSync10 } from "node:fs";
9132
9452
  import {
9133
9453
  MIXED_AUDIO_FILENAME,
9134
9454
  audioFailureDiagnostics,
@@ -9137,8 +9457,8 @@ import {
9137
9457
 
9138
9458
  // src/services/render/audioPadTrim.ts
9139
9459
  import { spawn } from "node:child_process";
9140
- import { mkdtempSync as mkdtempSync3, rmSync as rmSync8 } from "node:fs";
9141
- import { dirname as dirname7, join as join18 } from "node:path";
9460
+ import { mkdtempSync as mkdtempSync3, rmSync as rmSync9 } from "node:fs";
9461
+ import { dirname as dirname8, join as join19 } from "node:path";
9142
9462
  import {
9143
9463
  DEFAULT_CONFIG,
9144
9464
  describeFfmpegFailure as describeFfmpegFailure2,
@@ -9146,8 +9466,8 @@ import {
9146
9466
  extractAudioMetadata as extractAudioMetadata3,
9147
9467
  getFfprobeBinary,
9148
9468
  ManagedChildProcess,
9149
- renameWithRetry as renameWithRetry4,
9150
- runFfmpeg as runFfmpeg4,
9469
+ renameWithRetry as renameWithRetry5,
9470
+ runFfmpeg as runFfmpeg5,
9151
9471
  trackChildProcess
9152
9472
  } from "@hyperframes/engine";
9153
9473
  import { redactKnownPaths, redactTelemetryString as redactTelemetryString3 } from "@hyperframes/core";
@@ -9157,7 +9477,7 @@ import {
9157
9477
  aacSegmentCount,
9158
9478
  describeFfmpegFailure,
9159
9479
  extractAudioMetadata as extractAudioMetadata2,
9160
- runFfmpeg as runFfmpeg3
9480
+ runFfmpeg as runFfmpeg4
9161
9481
  } from "@hyperframes/engine";
9162
9482
  var toDbfs = (token) => token.toLowerCase() === "-inf" ? Number.NEGATIVE_INFINITY : Number(token);
9163
9483
  function checkedDbfs(value) {
@@ -9181,7 +9501,7 @@ async function scanTruePeak(audioPath, trim, timeoutMs, signal) {
9181
9501
  else laterBlocks = Math.max(laterBlocks, block);
9182
9502
  }
9183
9503
  };
9184
- const result = await runFfmpeg3(
9504
+ const result = await runFfmpeg4(
9185
9505
  ["-i", audioPath, "-map", "0:a:0", "-vn", "-af", `${trim}ebur128=peak=true`, "-f", "null", "-"],
9186
9506
  { signal, timeout: timeoutMs, onStderr: readLines }
9187
9507
  );
@@ -9341,7 +9661,7 @@ async function padOrTrimAudioToVideoFrameCount(input) {
9341
9661
  error: `audioPadTrim: failed to materialize ${plan.operation}: ${err instanceof Error ? err.message : String(err)}`
9342
9662
  };
9343
9663
  } finally {
9344
- for (const path of plan.cleanupPaths) rmSync8(path, { force: true });
9664
+ for (const path of plan.cleanupPaths) rmSync9(path, { force: true });
9345
9665
  }
9346
9666
  let loweredDb = 0;
9347
9667
  if (probeTruePeak) {
@@ -9388,8 +9708,8 @@ async function constrainAacTruePeak(input) {
9388
9708
  async function enforceAacTruePeak(input) {
9389
9709
  let scratchDir;
9390
9710
  try {
9391
- scratchDir = mkdtempSync3(join18(dirname7(input.audioPath), ".true-peak-"));
9392
- const correctedPath = join18(scratchDir, "audio.m4a");
9711
+ scratchDir = mkdtempSync3(join19(dirname8(input.audioPath), ".true-peak-"));
9712
+ const correctedPath = join19(scratchDir, "audio.m4a");
9393
9713
  const measurements = [];
9394
9714
  let attenuationDb = 0;
9395
9715
  let sourcePeakDbfs;
@@ -9404,7 +9724,7 @@ async function enforceAacTruePeak(input) {
9404
9724
  }
9405
9725
  if (truePeakDbfs <= AAC_DELIVERY_TRUE_PEAK_DBFS) {
9406
9726
  if (measuredPath !== correctedPath) return { success: true };
9407
- renameWithRetry4(correctedPath, input.audioPath);
9727
+ renameWithRetry5(correctedPath, input.audioPath);
9408
9728
  return { success: true, loweredDb: Number((-attenuationDb).toFixed(2)) };
9409
9729
  }
9410
9730
  if (pass === MAX_TRUE_PEAK_CORRECTION_PASSES) break;
@@ -9445,7 +9765,7 @@ async function enforceAacTruePeak(input) {
9445
9765
  )}`
9446
9766
  };
9447
9767
  } finally {
9448
- if (scratchDir) rmSync8(scratchDir, { recursive: true, force: true });
9768
+ if (scratchDir) rmSync9(scratchDir, { recursive: true, force: true });
9449
9769
  }
9450
9770
  }
9451
9771
  function failResult(outputPath, target, source, error) {
@@ -9522,7 +9842,7 @@ async function defaultProbeAudioInfo(audioPath, signal) {
9522
9842
  };
9523
9843
  }
9524
9844
  async function defaultRunFfmpeg(args, timeoutMs, signal) {
9525
- return toStepResult(await runFfmpeg4(args, { signal, timeout: timeoutMs }), timeoutMs);
9845
+ return toStepResult(await runFfmpeg5(args, { signal, timeout: timeoutMs }), timeoutMs);
9526
9846
  }
9527
9847
  function defaultAacEncoder(timeoutMs, signal, pcmSourcePath) {
9528
9848
  return async (inputPath, outputPath, durationSeconds, filter) => {
@@ -9592,7 +9912,7 @@ function assertAudioNotInterrupted(failures) {
9592
9912
  }
9593
9913
  async function runAudioStage(input) {
9594
9914
  const stageStart = Date.now();
9595
- const pcmSourcePath = join19(input.workDir, "audio-source.wav");
9915
+ const pcmSourcePath = join20(input.workDir, "audio-source.wav");
9596
9916
  const duration = input.totalFrames * input.fps.den / input.fps.num;
9597
9917
  try {
9598
9918
  const result = await mixAudioForDelivery(input, duration, pcmSourcePath);
@@ -9612,7 +9932,7 @@ async function runAudioStage(input) {
9612
9932
  }
9613
9933
  return { ...result, audioLoweredDb, audioProcessMs: Date.now() - stageStart };
9614
9934
  } finally {
9615
- rmSync9(pcmSourcePath, { force: true });
9935
+ rmSync10(pcmSourcePath, { force: true });
9616
9936
  }
9617
9937
  }
9618
9938
  async function mixAudioForDelivery(input, duration, pcmSourcePath) {
@@ -9630,7 +9950,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
9630
9950
  assertNotAborted,
9631
9951
  log
9632
9952
  } = input;
9633
- const audioOutputPath = join19(workDir, MIXED_AUDIO_FILENAME);
9953
+ const audioOutputPath = join20(workDir, MIXED_AUDIO_FILENAME);
9634
9954
  let hasAudio = false;
9635
9955
  let audioError;
9636
9956
  let audioFailures;
@@ -9641,7 +9961,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
9641
9961
  audioResult = await processCompositionAudio(
9642
9962
  audios,
9643
9963
  projectDir,
9644
- join19(workDir, "audio-work"),
9964
+ join20(workDir, "audio-work"),
9645
9965
  audioOutputPath,
9646
9966
  duration,
9647
9967
  abortSignal,
@@ -9710,7 +10030,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
9710
10030
 
9711
10031
  // src/services/render/stages/captureStage.ts
9712
10032
  import { existsSync as existsSync12, readdirSync as readdirSync3, statfsSync as statfsSync2 } from "node:fs";
9713
- import { join as join20 } from "node:path";
10033
+ import { join as join21 } from "node:path";
9714
10034
  import {
9715
10035
  classifyCaptureFailure as classifyCaptureFailure2,
9716
10036
  frameFileExtension,
@@ -9783,13 +10103,13 @@ function measureCaptureFrameBytes(workDir, framesDir) {
9783
10103
  if (!existsSync12(workDir)) return total;
9784
10104
  for (const attempt of readdirSync3(workDir)) {
9785
10105
  if (/^retry-\d+-batch-\d+-worker-\d+$/.test(attempt)) {
9786
- total += sampleDirectoryBytes(join20(workDir, attempt));
10106
+ total += sampleDirectoryBytes(join21(workDir, attempt));
9787
10107
  continue;
9788
10108
  }
9789
10109
  if (!/^capture-attempt-\d+$/.test(attempt)) continue;
9790
- const attemptDir = join20(workDir, attempt);
10110
+ const attemptDir = join21(workDir, attempt);
9791
10111
  for (const name of readdirSync3(attemptDir)) {
9792
- if (/^worker-\d+$/.test(name)) total += sampleDirectoryBytes(join20(attemptDir, name));
10112
+ if (/^worker-\d+$/.test(name)) total += sampleDirectoryBytes(join21(attemptDir, name));
9793
10113
  }
9794
10114
  }
9795
10115
  return total;
@@ -10082,8 +10402,8 @@ async function captureSessionFrames(session, { input, rangeStart, rangeEnd, resu
10082
10402
  }
10083
10403
 
10084
10404
  // src/services/render/stages/captureHdrStage.ts
10085
- import { existsSync as existsSync14, mkdirSync as mkdirSync7 } from "node:fs";
10086
- import { join as join24 } from "node:path";
10405
+ import { existsSync as existsSync14, mkdirSync as mkdirSync8 } from "node:fs";
10406
+ import { join as join25 } from "node:path";
10087
10407
  import {
10088
10408
  closeCaptureSession as closeCaptureSession7,
10089
10409
  cloneCaptureWarnings as cloneCaptureWarnings2,
@@ -10155,7 +10475,7 @@ function createHdrImageTransferCache(options = {}) {
10155
10475
  }
10156
10476
 
10157
10477
  // src/services/render/stages/captureHdrSequentialLoop.ts
10158
- import { join as join21 } from "node:path";
10478
+ import { join as join22 } from "node:path";
10159
10479
  import {
10160
10480
  TRANSITIONS,
10161
10481
  crossfade
@@ -10280,7 +10600,7 @@ async function runSequentialLayeredFrameLoop(input) {
10280
10600
  );
10281
10601
  if (debugDumpEnabled && debugDumpDir && i % 30 === 0) {
10282
10602
  writeFileExclusiveSync(
10283
- join21(debugDumpDir, `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`),
10603
+ join22(debugDumpDir, `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`),
10284
10604
  normalCanvas
10285
10605
  );
10286
10606
  }
@@ -10312,7 +10632,7 @@ async function runSequentialLayeredFrameLoop(input) {
10312
10632
  }
10313
10633
 
10314
10634
  // src/services/render/stages/captureHdrHybridLoop.ts
10315
- import { join as join23 } from "node:path";
10635
+ import { join as join24 } from "node:path";
10316
10636
  import {
10317
10637
  TRANSITIONS as TRANSITIONS2,
10318
10638
  closeCaptureSession as closeCaptureSession6,
@@ -10326,7 +10646,7 @@ import {
10326
10646
  // src/services/shaderTransitionWorkerPool.ts
10327
10647
  import { Worker } from "node:worker_threads";
10328
10648
  import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
10329
- import { dirname as dirname8, join as join22 } from "node:path";
10649
+ import { dirname as dirname9, join as join23 } from "node:path";
10330
10650
  import { createRequire } from "node:module";
10331
10651
  import { existsSync as existsSync13 } from "node:fs";
10332
10652
  import { cpus as cpus2 } from "node:os";
@@ -10339,10 +10659,10 @@ function resolveWorkerEntry(explicit) {
10339
10659
  const isTs = override.endsWith(".ts");
10340
10660
  return { path: override, isTs };
10341
10661
  }
10342
- const moduleDir = dirname8(fileURLToPath3(import.meta.url));
10343
- const jsPath = join22(moduleDir, "shaderTransitionWorker.js");
10662
+ const moduleDir = dirname9(fileURLToPath3(import.meta.url));
10663
+ const jsPath = join23(moduleDir, "shaderTransitionWorker.js");
10344
10664
  if (existsSync13(jsPath)) return { path: jsPath, isTs: false };
10345
- const tsPath = join22(moduleDir, "shaderTransitionWorker.ts");
10665
+ const tsPath = join23(moduleDir, "shaderTransitionWorker.ts");
10346
10666
  return { path: tsPath, isTs: true };
10347
10667
  }
10348
10668
  function buildExecArgv(entryIsTs) {
@@ -10485,8 +10805,8 @@ async function createShaderTransitionWorkerPool(opts) {
10485
10805
  if (terminated) {
10486
10806
  throw new Error("shader-blend pool already terminated");
10487
10807
  }
10488
- return new Promise((resolve10, reject) => {
10489
- const task = traceEnabled ? { req, resolve: resolve10, reject, enqueuedAtMs: Date.now(), traceId: ++nextTaskId } : { req, resolve: resolve10, reject };
10808
+ return new Promise((resolve11, reject) => {
10809
+ const task = traceEnabled ? { req, resolve: resolve11, reject, enqueuedAtMs: Date.now(), traceId: ++nextTaskId } : { req, resolve: resolve11, reject };
10490
10810
  const idle = slots.find((s) => !s.busy && !s.dead);
10491
10811
  if (idle) {
10492
10812
  queue.unshift(task);
@@ -10710,7 +11030,7 @@ async function runHybridLayeredFrameLoop(input) {
10710
11030
  );
10711
11031
  if (debugDumpEnabled && debugDumpDir && i % 30 === 0) {
10712
11032
  writeFileExclusiveSync(
10713
- join23(debugDumpDir, `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`),
11033
+ join24(debugDumpDir, `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`),
10714
11034
  canvas
10715
11035
  );
10716
11036
  }
@@ -10898,9 +11218,9 @@ async function runCaptureHdrStage(input) {
10898
11218
  if (hdrVideoFrameSources.has(v.id)) hdrVideoEndTimes.set(v.id, v.end);
10899
11219
  }
10900
11220
  const debugDumpEnabled = process.env.KEEP_TEMP === "1";
10901
- const debugDumpDir = debugDumpEnabled ? join24(framesDir, "debug-composite") : null;
11221
+ const debugDumpDir = debugDumpEnabled ? join25(framesDir, "debug-composite") : null;
10902
11222
  if (debugDumpDir && !existsSync14(debugDumpDir)) {
10903
- mkdirSync7(debugDumpDir, { recursive: true });
11223
+ mkdirSync8(debugDumpDir, { recursive: true });
10904
11224
  }
10905
11225
  const compositeTransfer = resolveCompositeTransfer(hasHdrContent, effectiveHdr);
10906
11226
  const hdrTargetTransfer = compositeTransfer === "srgb" ? void 0 : compositeTransfer;
@@ -11053,24 +11373,24 @@ async function runCaptureHdrStage(input) {
11053
11373
 
11054
11374
  // src/services/render/stages/encodeStage.ts
11055
11375
  import {
11056
- copyFileSync as copyFileSync3,
11376
+ copyFileSync as copyFileSync4,
11057
11377
  existsSync as existsSync15,
11058
- mkdirSync as mkdirSync8,
11378
+ mkdirSync as mkdirSync9,
11059
11379
  readdirSync as readdirSync4,
11060
- readFileSync as readFileSync7,
11061
- rmSync as rmSync10,
11062
- statSync as statSync6,
11063
- writeFileSync as writeFileSync4
11380
+ readFileSync as readFileSync8,
11381
+ rmSync as rmSync11,
11382
+ statSync as statSync7,
11383
+ writeFileSync as writeFileSync5
11064
11384
  } from "node:fs";
11065
11385
  import { readFile as readFile2 } from "node:fs/promises";
11066
- import { dirname as dirname9, join as join25 } from "node:path";
11386
+ import { dirname as dirname10, join as join26 } from "node:path";
11067
11387
  import {
11068
11388
  encodeFramesChunkedConcat,
11069
11389
  encodeFramesFromDir,
11070
11390
  formatFfmpegError,
11071
11391
  MIXED_AUDIO_FILENAME as MIXED_AUDIO_FILENAME2,
11072
11392
  resolveConfig,
11073
- runFfmpeg as runFfmpeg5,
11393
+ runFfmpeg as runFfmpeg6,
11074
11394
  spawnStreamingEncoder as spawnStreamingEncoder4
11075
11395
  } from "@hyperframes/engine";
11076
11396
  import {
@@ -11146,17 +11466,17 @@ function gifEncodeFailure(startTime, outputPath, error, failureReason) {
11146
11466
  }
11147
11467
  async function reencodeWithClearedWholeFrames(argsInput, run, startTime) {
11148
11468
  const { outputPath } = argsInput;
11149
- const result = await runFfmpeg5(buildGifPaletteuseArgs({ ...argsInput, wholeFrames: true }), run);
11469
+ const result = await runFfmpeg6(buildGifPaletteuseArgs({ ...argsInput, wholeFrames: true }), run);
11150
11470
  if (!result.success) {
11151
11471
  const error = formatFfmpegError(result.exitCode, result.stderr);
11152
11472
  return gifEncodeFailure(startTime, outputPath, error, result.failureReason);
11153
11473
  }
11154
- const gif = readFileSync7(outputPath);
11474
+ const gif = readFileSync8(outputPath);
11155
11475
  if (!clearGifFramesBeforeNext(gif)) {
11156
11476
  const error = "[FFmpeg] GIF output could not be parsed to clear its frames";
11157
11477
  return gifEncodeFailure(startTime, outputPath, error);
11158
11478
  }
11159
- writeFileSync4(outputPath, gif);
11479
+ writeFileSync5(outputPath, gif);
11160
11480
  return null;
11161
11481
  }
11162
11482
  async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
@@ -11178,17 +11498,17 @@ async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
11178
11498
  try {
11179
11499
  const run = { signal: input.signal, timeout: input.timeout };
11180
11500
  for (const args of [buildGifPalettegenArgs(argsInput), buildGifPaletteuseArgs(argsInput)]) {
11181
- const result = await runFfmpeg5(args, run);
11501
+ const result = await runFfmpeg6(args, run);
11182
11502
  if (!result.success) {
11183
11503
  const error = formatFfmpegError(result.exitCode, result.stderr);
11184
11504
  return gifEncodeFailure(startTime, outputPath, error, result.failureReason);
11185
11505
  }
11186
11506
  }
11187
- if (input.preserveAlpha && gifClearsAfterLeavingFrameInPlace(readFileSync7(outputPath)) !== false) {
11507
+ if (input.preserveAlpha && gifClearsAfterLeavingFrameInPlace(readFileSync8(outputPath)) !== false) {
11188
11508
  const failure = await reencodeWithClearedWholeFrames(argsInput, run, startTime);
11189
11509
  if (failure) return failure;
11190
11510
  }
11191
- const fileSize = existsSync15(outputPath) ? statSync6(outputPath).size : 0;
11511
+ const fileSize = existsSync15(outputPath) ? statSync7(outputPath).size : 0;
11192
11512
  return {
11193
11513
  success: true,
11194
11514
  outputPath,
@@ -11197,7 +11517,7 @@ async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
11197
11517
  fileSize
11198
11518
  };
11199
11519
  } finally {
11200
- rmSync10(input.palettePath, { force: true });
11520
+ rmSync11(input.palettePath, { force: true });
11201
11521
  }
11202
11522
  }
11203
11523
  function startEncodeProgress(job, stage, onProgress) {
@@ -11269,10 +11589,10 @@ function encodeWhileCapturing(input, totalFrames) {
11269
11589
  const stream = await encoder;
11270
11590
  for (let next = 0; next < totalFrames; next++) {
11271
11591
  while (!landed.has(next) && !captureDone && !signal.aborted) {
11272
- await new Promise((resolve10) => wake = resolve10);
11592
+ await new Promise((resolve11) => wake = resolve11);
11273
11593
  }
11274
11594
  if (signal.aborted) return;
11275
- const mergedPath = join25(input.framesDir, formatCaptureFrameName(next, "jpg"));
11595
+ const mergedPath = join26(input.framesDir, formatCaptureFrameName(next, "jpg"));
11276
11596
  const frame = await readLandedFrame(landed.get(next), mergedPath);
11277
11597
  landed.delete(next);
11278
11598
  if (!await stream.writeFrame(frame)) {
@@ -11347,7 +11667,7 @@ async function runEncodeStage(input) {
11347
11667
  const stage5Start = Date.now();
11348
11668
  if (isPngSequence) {
11349
11669
  const encoded = startEncodeProgress(job, "Writing PNG sequence", onProgress);
11350
- if (!existsSync15(outputPath)) mkdirSync8(outputPath, { recursive: true });
11670
+ if (!existsSync15(outputPath)) mkdirSync9(outputPath, { recursive: true });
11351
11671
  const captured = readdirSync4(framesDir).filter((name) => name.endsWith(".png")).sort();
11352
11672
  if (captured.length === 0) {
11353
11673
  throw new Error(
@@ -11355,11 +11675,11 @@ async function runEncodeStage(input) {
11355
11675
  );
11356
11676
  }
11357
11677
  captured.forEach((name, i) => {
11358
- const dst = join25(outputPath, formatExportFrameName(i, "png"));
11359
- copyFileSync3(join25(framesDir, name), dst);
11678
+ const dst = join26(outputPath, formatExportFrameName(i, "png"));
11679
+ copyFileSync4(join26(framesDir, name), dst);
11360
11680
  });
11361
11681
  if (hasAudio && audioOutputPath && existsSync15(audioOutputPath)) {
11362
- copyFileSync3(audioOutputPath, join25(outputPath, MIXED_AUDIO_FILENAME2));
11682
+ copyFileSync4(audioOutputPath, join26(outputPath, MIXED_AUDIO_FILENAME2));
11363
11683
  log.info(
11364
11684
  `[Render] png-sequence: ${MIXED_AUDIO_FILENAME2} sidecar written to ${outputPath}/${MIXED_AUDIO_FILENAME2}`
11365
11685
  );
@@ -11379,7 +11699,7 @@ async function runEncodeStage(input) {
11379
11699
  const encodeResult2 = await encodeGifFromDir(framesDir, framePattern2, outputPath, {
11380
11700
  fps: job.config.fps,
11381
11701
  loop,
11382
- palettePath: join25(dirname9(videoOnlyPath), "gif-palette.png"),
11702
+ palettePath: join26(dirname10(videoOnlyPath), "gif-palette.png"),
11383
11703
  preserveAlpha: needsAlpha,
11384
11704
  signal: abortSignal,
11385
11705
  timeout: engineCfg.ffmpegEncodeTimeout
@@ -11589,9 +11909,9 @@ function sampleDirectoryBytes(dir) {
11589
11909
  continue;
11590
11910
  }
11591
11911
  for (const name of entries) {
11592
- const full = join26(current, name);
11912
+ const full = join27(current, name);
11593
11913
  try {
11594
- const st = statSync7(full);
11914
+ const st = statSync8(full);
11595
11915
  if (st.isDirectory()) {
11596
11916
  stack.push(full);
11597
11917
  } else if (st.isFile()) {
@@ -11738,8 +12058,8 @@ function findMissingFrameRanges(totalFrames, framesDir, frameExt) {
11738
12058
  const ranges = [];
11739
12059
  let rangeStart = null;
11740
12060
  for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
11741
- const framePath = join26(framesDir, formatCaptureFrameName(frameIndex, frameExt));
11742
- const missing = !existsSync16(framePath) || statSync7(framePath).size <= 8;
12061
+ const framePath = join27(framesDir, formatCaptureFrameName(frameIndex, frameExt));
12062
+ const missing = !existsSync16(framePath) || statSync8(framePath).size <= 8;
11743
12063
  if (missing && rangeStart === null) {
11744
12064
  rangeStart = frameIndex;
11745
12065
  } else if (!missing && rangeStart !== null) {
@@ -11755,7 +12075,7 @@ function findMissingFrameRanges(totalFrames, framesDir, frameExt) {
11755
12075
  function buildMissingFrameRetryBatches(ranges, maxWorkers, workDir, attempt, rangeStart = 0, stride = 1) {
11756
12076
  const workersPerBatch = Math.max(1, Math.floor(maxWorkers));
11757
12077
  const batches = [];
11758
- const outputDirFor = (batchIndex, workerId) => join26(workDir, `retry-${attempt}-batch-${batchIndex}-worker-${workerId}`);
12078
+ const outputDirFor = (batchIndex, workerId) => join27(workDir, `retry-${attempt}-batch-${batchIndex}-worker-${workerId}`);
11759
12079
  if (stride > 1) {
11760
12080
  const spans = /* @__PURE__ */ new Map();
11761
12081
  for (const range of ranges) {
@@ -11815,8 +12135,8 @@ function getNextRetryWorkerCount(currentWorkers) {
11815
12135
  return Math.max(1, Math.floor(currentWorkers / 2));
11816
12136
  }
11817
12137
  function resolveRenderWorkDirPrefix(outputPath, jobId, platform2 = process.platform, systemTempDir = tmpdir2()) {
11818
- if (platform2 === "win32" || platform2 === "darwin") return join26(systemTempDir, "hf-render-");
11819
- return join26(dirname10(outputPath), `work-${jobId}-`);
12138
+ if (platform2 === "win32" || platform2 === "darwin") return join27(systemTempDir, "hf-render-");
12139
+ return join27(dirname11(outputPath), `work-${jobId}-`);
11820
12140
  }
11821
12141
  var MAX_TRANSIENT_CAPTURE_RETRIES = 1;
11822
12142
  function isTransientCaptureRetryEligible(failureKind, missing, retriesUsed) {
@@ -11858,7 +12178,7 @@ The composition is too large for the available memory. To reduce memory pressure
11858
12178
  function countCapturedFrames(totalFrames, framesDir, frameExt) {
11859
12179
  let captured = 0;
11860
12180
  for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
11861
- const framePath = join26(framesDir, formatCaptureFrameName(frameIndex, frameExt));
12181
+ const framePath = join27(framesDir, formatCaptureFrameName(frameIndex, frameExt));
11862
12182
  if (existsSync16(framePath)) captured++;
11863
12183
  }
11864
12184
  return captured;
@@ -11884,7 +12204,7 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
11884
12204
  reason: attempt === 0 ? "initial" : pendingTransientRetry ? "transient-retry" : "retry"
11885
12205
  });
11886
12206
  pendingTransientRetry = false;
11887
- const attemptWorkDir = join26(options.workDir, `capture-attempt-${attempt}`);
12207
+ const attemptWorkDir = join27(options.workDir, `capture-attempt-${attempt}`);
11888
12208
  const batches = missingRanges ? buildMissingFrameRetryBatches(
11889
12209
  missingRanges,
11890
12210
  currentWorkers,
@@ -12490,18 +12810,18 @@ function deVerifyFallbackTelemetry(err) {
12490
12810
  };
12491
12811
  }
12492
12812
  function resolveRenderDebugDir() {
12493
- const producerRoot = process.env.PRODUCER_RENDERS_DIR ? resolve7(process.env.PRODUCER_RENDERS_DIR, "..") : resolve7(dirname10(fileURLToPath4(import.meta.url)), "../..");
12494
- return join26(producerRoot, ".debug");
12813
+ const producerRoot = process.env.PRODUCER_RENDERS_DIR ? resolve8(process.env.PRODUCER_RENDERS_DIR, "..") : resolve8(dirname11(fileURLToPath4(import.meta.url)), "../..");
12814
+ return join27(producerRoot, ".debug");
12495
12815
  }
12496
12816
  async function executeRenderJob(job, projectDir, outputPath, progressSink, abortSignal, assertRenderActive) {
12497
12817
  validateHlsRenderConfig(job.config);
12498
12818
  const debugDir = resolveRenderDebugDir();
12499
- const outputDir = dirname10(outputPath);
12500
- if (!existsSync16(outputDir)) mkdirSync9(outputDir, { recursive: true });
12501
- const workDir = job.config.debug ? join26(debugDir, job.id) : createRenderWorkDir(resolveRenderWorkDirPrefix(outputPath, job.id), outputDir);
12819
+ const outputDir = dirname11(outputPath);
12820
+ if (!existsSync16(outputDir)) mkdirSync10(outputDir, { recursive: true });
12821
+ const workDir = job.config.debug ? join27(debugDir, job.id) : createRenderWorkDir(resolveRenderWorkDirPrefix(outputPath, job.id), outputDir);
12502
12822
  const pipelineStart = Date.now();
12503
12823
  const baseLog = job.config.logger ?? defaultLogger;
12504
- const logPath = job.config.debug ? join26(workDir, "render.log") : null;
12824
+ const logPath = job.config.debug ? join27(workDir, "render.log") : null;
12505
12825
  const execution = new RenderExecutionContext({
12506
12826
  request: { renderJobId: job.id, projectDir, outputPath },
12507
12827
  logger: logPath ? createRenderFileLogger(logPath, baseLog) : baseLog,
@@ -12515,7 +12835,7 @@ async function executeRenderJob(job, projectDir, outputPath, progressSink, abort
12515
12835
  log.info("KEEP_TEMP=1, leaving workDir on disk for inspection", { workDir });
12516
12836
  return;
12517
12837
  }
12518
- rmSync11(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
12838
+ rmSync12(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
12519
12839
  });
12520
12840
  try {
12521
12841
  await executeRenderPipeline({
@@ -12561,7 +12881,7 @@ async function executeRenderPipeline(input) {
12561
12881
  imageDecodeFailures: 0
12562
12882
  };
12563
12883
  let hdrPerf;
12564
- const perfOutputPath = join26(workDir, "perf-summary.json");
12884
+ const perfOutputPath = join27(workDir, "perf-summary.json");
12565
12885
  const cfg = { ...job.config.producerConfig ?? resolveConfig2() };
12566
12886
  const observability = new RenderObservabilityRecorder({
12567
12887
  pipelineStartMs: pipelineStart,
@@ -12634,7 +12954,7 @@ async function executeRenderPipeline(input) {
12634
12954
  job.startedAt = /* @__PURE__ */ new Date();
12635
12955
  assertNotAborted();
12636
12956
  assertConfiguredFfmpegBinariesExist();
12637
- if (!existsSync16(workDir)) mkdirSync9(workDir, { recursive: true });
12957
+ if (!existsSync16(workDir)) mkdirSync10(workDir, { recursive: true });
12638
12958
  if (job.config.debug) {
12639
12959
  log.info("[Render] Debug artifacts enabled", { workDir, logPath });
12640
12960
  }
@@ -12663,22 +12983,22 @@ async function executeRenderPipeline(input) {
12663
12983
  requestedWorkers: job.config.workers ?? "auto"
12664
12984
  });
12665
12985
  const entryFile = job.config.entryFile || "index.html";
12666
- let htmlPath = join26(projectDir, entryFile);
12986
+ let htmlPath = join27(projectDir, entryFile);
12667
12987
  if (!existsSync16(htmlPath)) {
12668
12988
  throw new Error(`Entry file not found: ${htmlPath}`);
12669
12989
  }
12670
12990
  assertNotAborted();
12671
- const rawEntry = readFileSync8(htmlPath, "utf-8");
12991
+ const rawEntry = readFileSync9(htmlPath, "utf-8");
12672
12992
  if (entryFile !== "index.html" && isTemplateEntry(rawEntry)) {
12673
- const wrapperPath = join26(workDir, "standalone-entry.html");
12674
- const projectIndexPath = join26(projectDir, "index.html");
12993
+ const wrapperPath = join27(workDir, "standalone-entry.html");
12994
+ const projectIndexPath = join27(projectDir, "index.html");
12675
12995
  if (!existsSync16(projectIndexPath)) {
12676
12996
  throw new Error(
12677
12997
  `Template entry file "${entryFile}" requires a project index.html to extract its render shell.`
12678
12998
  );
12679
12999
  }
12680
13000
  const standaloneHtml = extractStandaloneEntryFromIndex(
12681
- readFileSync8(projectIndexPath, "utf-8"),
13001
+ readFileSync9(projectIndexPath, "utf-8"),
12682
13002
  entryFile,
12683
13003
  rawEntry
12684
13004
  );
@@ -12687,7 +13007,7 @@ async function executeRenderPipeline(input) {
12687
13007
  `Entry file "${entryFile}" is not mounted from index.html via data-composition-src, so it cannot be rendered independently.`
12688
13008
  );
12689
13009
  }
12690
- writeFileSync5(wrapperPath, standaloneHtml, "utf-8");
13010
+ writeFileSync6(wrapperPath, standaloneHtml, "utf-8");
12691
13011
  htmlPath = wrapperPath;
12692
13012
  log.info("Extracted standalone entry from index.html host context", {
12693
13013
  entryFile
@@ -12834,7 +13154,7 @@ async function executeRenderPipeline(input) {
12834
13154
  updateJobStatus(job, "preprocessing", "Extracting video frames", 10, onProgress, {
12835
13155
  code: "extract_video"
12836
13156
  });
12837
- const compiledDir = join26(workDir, "compiled");
13157
+ const compiledDir = join27(workDir, "compiled");
12838
13158
  const extractResult = await observeRenderStage(
12839
13159
  observability,
12840
13160
  "video_extract",
@@ -12968,7 +13288,7 @@ async function executeRenderPipeline(input) {
12968
13288
  try {
12969
13289
  fileServer = await createFileServer({
12970
13290
  projectDir,
12971
- compiledDir: join26(workDir, "compiled"),
13291
+ compiledDir: join27(workDir, "compiled"),
12972
13292
  port: 0,
12973
13293
  preHeadScripts: [VIRTUAL_TIME_SHIM],
12974
13294
  fps: job.config.fps
@@ -12986,8 +13306,8 @@ async function executeRenderPipeline(input) {
12986
13306
  if (!activeFileServer) {
12987
13307
  throw new Error("File server failed to initialize before frame capture");
12988
13308
  }
12989
- const framesDir = join26(workDir, "captured-frames");
12990
- if (!existsSync16(framesDir)) mkdirSync9(framesDir, { recursive: true });
13309
+ const framesDir = join27(workDir, "captured-frames");
13310
+ if (!existsSync16(framesDir)) mkdirSync10(framesDir, { recursive: true });
12991
13311
  updateJobStatus(job, "rendering", "Checking browser GPU", 25, onProgress, {
12992
13312
  code: "start_browsers"
12993
13313
  });
@@ -13458,7 +13778,7 @@ async function executeRenderPipeline(input) {
13458
13778
  hls: ".mp4"
13459
13779
  };
13460
13780
  const videoExt = FORMAT_EXT[outputFormat] ?? ".mp4";
13461
- const videoOnlyPath = join26(workDir, `video-only${videoExt}`);
13781
+ const videoOnlyPath = join27(workDir, `video-only${videoExt}`);
13462
13782
  const usePageSideCompositingForTransitions = (cfg.enablePageSideCompositing || isGif) && compiled.hasShaderTransitions && !hasHdrContent && outputSupportsPageSideShaderCompositing(outputFormat);
13463
13783
  if (usePageSideCompositingForTransitions) {
13464
13784
  activeFileServer.addPreHeadScript(HF_PAGE_SIDE_COMPOSITING_STUB);
@@ -13711,7 +14031,7 @@ async function executeRenderPipeline(input) {
13711
14031
  pageMotionBlur: job.config.pageMotionBlur,
13712
14032
  captureQuality: captureOptions.quality
13713
14033
  });
13714
- const segmentDir = segmentDirFor(join26(projectDir, "renders"), segmentPlanHash);
14034
+ const segmentDir = segmentDirFor(join27(projectDir, "renders"), segmentPlanHash);
13715
14035
  let completedSegments = /* @__PURE__ */ new Set();
13716
14036
  if (job.config.resumeSegments === true) {
13717
14037
  const existing = readSegmentManifest(segmentDir);
@@ -13726,10 +14046,10 @@ async function executeRenderPipeline(input) {
13726
14046
  });
13727
14047
  }
13728
14048
  if (completedSegments.size === 0) {
13729
- rmSync11(segmentDir, { recursive: true, force: true });
14049
+ rmSync12(segmentDir, { recursive: true, force: true });
13730
14050
  }
13731
14051
  } else {
13732
- rmSync11(segmentDir, { recursive: true, force: true });
14052
+ rmSync12(segmentDir, { recursive: true, force: true });
13733
14053
  }
13734
14054
  const segmentManifest = readSegmentManifest(segmentDir) ?? {
13735
14055
  version: 1,
@@ -13810,7 +14130,7 @@ async function executeRenderPipeline(input) {
13810
14130
  );
13811
14131
  updateCaptureObservability({ segmentRetries: segmentedRes.segmentRetries });
13812
14132
  if (job.config.keepSegments !== true) {
13813
- rmSync11(segmentDir, { recursive: true, force: true });
14133
+ rmSync12(segmentDir, { recursive: true, force: true });
13814
14134
  }
13815
14135
  } else {
13816
14136
  capturePlan = replanAfterFailure(capturePlan, { kind: "streaming_unavailable" });
@@ -14082,8 +14402,8 @@ async function executeRenderPipeline(input) {
14082
14402
  "capture_disk",
14083
14403
  "drawElement capture failed; retrying with a fresh screenshot session"
14084
14404
  );
14085
- rmSync11(framesDir, { recursive: true, force: true });
14086
- mkdirSync9(framesDir, { recursive: true });
14405
+ rmSync12(framesDir, { recursive: true, force: true });
14406
+ mkdirSync10(framesDir, { recursive: true });
14087
14407
  resetCaptureAttemptProgress(job);
14088
14408
  dedupPerfs.length = 0;
14089
14409
  cfg.useDrawElement = false;
@@ -14259,7 +14579,7 @@ async function executeRenderPipeline(input) {
14259
14579
  job.perfSummary = perfSummary;
14260
14580
  if (job.config.debug) {
14261
14581
  try {
14262
- writeFileSync5(perfOutputPath, JSON.stringify(perfSummary, null, 2), "utf-8");
14582
+ writeFileSync6(perfOutputPath, JSON.stringify(perfSummary, null, 2), "utf-8");
14263
14583
  } catch (err) {
14264
14584
  log.debug("Failed to write perf summary", {
14265
14585
  perfOutputPath,
@@ -14269,8 +14589,8 @@ async function executeRenderPipeline(input) {
14269
14589
  }
14270
14590
  if (job.config.debug) {
14271
14591
  if (!isPngSequence && !isHls && existsSync16(stagedOutputPath)) {
14272
- const debugOutput = join26(workDir, `output${videoExt}`);
14273
- copyFileSync4(stagedOutputPath, debugOutput);
14592
+ const debugOutput = join27(workDir, `output${videoExt}`);
14593
+ copyFileSync5(stagedOutputPath, debugOutput);
14274
14594
  }
14275
14595
  }
14276
14596
  await commitArtifactTransaction(artifactTransaction, () => {
@@ -14375,14 +14695,14 @@ async function executeRenderPipeline(input) {
14375
14695
  // src/services/hyperframeLint.ts
14376
14696
  import {
14377
14697
  existsSync as existsSync17,
14378
- readFileSync as readFileSync9,
14379
- statSync as statSync8,
14380
- openSync as openSync4,
14698
+ readFileSync as readFileSync10,
14699
+ statSync as statSync9,
14700
+ openSync as openSync5,
14381
14701
  fstatSync as fstatSync3,
14382
- closeSync as closeSync4,
14702
+ closeSync as closeSync5,
14383
14703
  constants as constants3
14384
14704
  } from "node:fs";
14385
- import { resolve as resolve8, join as join27, relative as relative3, isAbsolute as isAbsolute3, sep as sep2 } from "node:path";
14705
+ import { resolve as resolve9, join as join28, relative as relative3, isAbsolute as isAbsolute3, sep as sep2 } from "node:path";
14386
14706
  import { lintHyperframeHtml } from "@hyperframes/lint";
14387
14707
  function isStringRecord(value) {
14388
14708
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -14412,30 +14732,30 @@ function pickEntryFile(files, preferredEntryFile) {
14412
14732
  function readEntryHtml(filePath) {
14413
14733
  let fd;
14414
14734
  try {
14415
- fd = openSync4(filePath, constants3.O_RDONLY | constants3.O_NONBLOCK);
14735
+ fd = openSync5(filePath, constants3.O_RDONLY | constants3.O_NONBLOCK);
14416
14736
  } catch (error) {
14417
14737
  if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR"))
14418
14738
  return null;
14419
- if (!statSync8(filePath, { throwIfNoEntry: false })?.isFile()) return null;
14739
+ if (!statSync9(filePath, { throwIfNoEntry: false })?.isFile()) return null;
14420
14740
  throw error;
14421
14741
  }
14422
14742
  try {
14423
14743
  if (!fstatSync3(fd).isFile()) return null;
14424
- return readFileSync9(fd, "utf-8");
14744
+ return readFileSync10(fd, "utf-8");
14425
14745
  } finally {
14426
- closeSync4(fd);
14746
+ closeSync5(fd);
14427
14747
  }
14428
14748
  }
14429
14749
  function readProjectEntryFile(projectDir, preferredEntryFile) {
14430
- const absProjectDir = resolve8(projectDir);
14431
- if (!existsSync17(absProjectDir) || !statSync8(absProjectDir).isDirectory()) {
14750
+ const absProjectDir = resolve9(projectDir);
14751
+ if (!existsSync17(absProjectDir) || !statSync9(absProjectDir).isDirectory()) {
14432
14752
  return { error: `Project directory not found: ${absProjectDir}` };
14433
14753
  }
14434
14754
  const entryCandidates = [preferredEntryFile, "index.html", "src/index.html"].filter(
14435
14755
  (value) => typeof value === "string" && value.trim().length > 0
14436
14756
  );
14437
14757
  for (const entryFile of entryCandidates) {
14438
- const absoluteEntryPath = resolve8(absProjectDir, entryFile);
14758
+ const absoluteEntryPath = resolve9(absProjectDir, entryFile);
14439
14759
  const entryRelativePath = relative3(absProjectDir, absoluteEntryPath);
14440
14760
  if (entryRelativePath === ".." || entryRelativePath.startsWith(`..${sep2}`) || isAbsolute3(entryRelativePath)) {
14441
14761
  return { error: `Entry file must stay inside project directory: ${entryFile}` };
@@ -14450,7 +14770,7 @@ function readProjectEntryFile(projectDir, preferredEntryFile) {
14450
14770
  }
14451
14771
  }
14452
14772
  return {
14453
- error: `No HTML entry file found in project directory: ${join27(absProjectDir, preferredEntryFile || "index.html")}`
14773
+ error: `No HTML entry file found in project directory: ${join28(absProjectDir, preferredEntryFile || "index.html")}`
14454
14774
  };
14455
14775
  }
14456
14776
  function prepareHyperframeLintBody(body) {
@@ -14496,7 +14816,7 @@ async function runHyperframeLint(prepared) {
14496
14816
  // src/services/healthWorker.ts
14497
14817
  import { Worker as Worker2 } from "node:worker_threads";
14498
14818
  import { fileURLToPath as fileURLToPath5 } from "node:url";
14499
- import { dirname as dirname11, join as join28 } from "node:path";
14819
+ import { dirname as dirname12, join as join29 } from "node:path";
14500
14820
  import { existsSync as existsSync18 } from "node:fs";
14501
14821
  var DEFAULT_HEALTH_PORT = 9848;
14502
14822
  async function startHealthWorker(options = {}) {
@@ -14515,12 +14835,12 @@ async function startHealthWorker(options = {}) {
14515
14835
  stdout: false,
14516
14836
  stderr: false
14517
14837
  });
14518
- await new Promise((resolve10, reject) => {
14838
+ await new Promise((resolve11, reject) => {
14519
14839
  const onMessage = (msg) => {
14520
14840
  if (msg?.type === "listening") {
14521
14841
  worker.off("error", onError);
14522
14842
  worker.off("message", onMessage);
14523
- resolve10();
14843
+ resolve11();
14524
14844
  } else if (msg?.type === "listen-error") {
14525
14845
  worker.off("error", onError);
14526
14846
  worker.off("message", onMessage);
@@ -14571,8 +14891,8 @@ function defaultLogger2() {
14571
14891
  };
14572
14892
  }
14573
14893
  function resolveWorkerEntry2() {
14574
- const here = dirname11(fileURLToPath5(import.meta.url));
14575
- const candidates = [join28(here, "healthWorkerThread.js"), join28(here, "healthWorkerThread.ts")];
14894
+ const here = dirname12(fileURLToPath5(import.meta.url));
14895
+ const candidates = [join29(here, "healthWorkerThread.js"), join29(here, "healthWorkerThread.ts")];
14576
14896
  for (const candidate of candidates) {
14577
14897
  if (existsSync18(candidate)) return candidate;
14578
14898
  }
@@ -14608,7 +14928,7 @@ import { DISTRIBUTED_FPS, isDistributedFps } from "@hyperframes/core";
14608
14928
 
14609
14929
  // src/services/distributed/plan.ts
14610
14930
  import { audioFailureDiagnostics as audioFailureDiagnostics3 } from "@hyperframes/engine";
14611
- import { extname as extname4, isAbsolute as isAbsolute4, join as join29, relative as relative4, sep as sep3 } from "node:path";
14931
+ import { extname as extname4, isAbsolute as isAbsolute4, join as join30, relative as relative4, sep as sep3 } from "node:path";
14612
14932
  import {
14613
14933
  fpsToNumber as fpsToNumber10,
14614
14934
  HF_COLOR_GRADING_ATTR as HF_COLOR_GRADING_ATTR2
@@ -14618,7 +14938,7 @@ import {
14618
14938
  normalizeVp9CpuUsed,
14619
14939
  resolveConfig as resolveConfig3,
14620
14940
  declareContentAddressedUrl,
14621
- renameWithRetry as renameWithRetry6,
14941
+ renameWithRetry as renameWithRetry7,
14622
14942
  declareProbeFactsUrl
14623
14943
  } from "@hyperframes/engine";
14624
14944
 
@@ -14689,10 +15009,10 @@ var DISTRIBUTED_RENDER_CAPABILITIES = Object.freeze({
14689
15009
  var FIELD_DELIMITER = Buffer.from([0]);
14690
15010
 
14691
15011
  // src/services/distributed/chunkAudio.ts
14692
- import { appendRenderProvenanceArgs, describeFfmpegFailure as describeFfmpegFailure3, runFfmpeg as runFfmpeg6 } from "@hyperframes/engine";
15012
+ import { appendRenderProvenanceArgs, describeFfmpegFailure as describeFfmpegFailure3, runFfmpeg as runFfmpeg7 } from "@hyperframes/engine";
14693
15013
 
14694
15014
  // src/services/distributed/mp4Stitch.ts
14695
- import { renameWithRetry as renameWithRetry5 } from "@hyperframes/engine";
15015
+ import { renameWithRetry as renameWithRetry6 } from "@hyperframes/engine";
14696
15016
  var SCAN_BYTES = 64 * 1024;
14697
15017
 
14698
15018
  // src/services/distributed/chunkAudio.ts
@@ -14712,9 +15032,9 @@ var PLAN_ROOT_CATEGORY = {
14712
15032
  var PLAN_DIR_SIZE_LIMIT_BYTES = 2 * 1024 * 1024 * 1024;
14713
15033
  var FREEZE_OWNED_PLAN_FILES = [
14714
15034
  "plan.json",
14715
- join29("meta", "composition.json"),
14716
- join29("meta", "encoder.json"),
14717
- join29("meta", "chunks.json")
15035
+ join30("meta", "composition.json"),
15036
+ join30("meta", "encoder.json"),
15037
+ join30("meta", "chunks.json")
14718
15038
  ];
14719
15039
 
14720
15040
  // src/services/distributed/renderConfigValidation.ts
@@ -14945,6 +15265,8 @@ var DEFAULT_SERVER_FPS = { num: 30, den: 1 };
14945
15265
  var SSE_KEEPALIVE_MS = 3e4;
14946
15266
  var SAFE_RENDER_ERROR_CODES = /* @__PURE__ */ new Set([
14947
15267
  "ASSET_MEDIA_TYPE_MISMATCH",
15268
+ "HEIF_IMAGE_CONVERSION_FAILED",
15269
+ "HEIF_TOOL_UNAVAILABLE",
14948
15270
  "NOT_MEDIA_PAYLOAD",
14949
15271
  "INVALID_VIDEO_METADATA",
14950
15272
  "VIDEO_SOURCE_UNRENDERABLE",
@@ -15080,8 +15402,8 @@ function buildRenderJobConfig(input, outputPath, log) {
15080
15402
  function resolvePreparedRenderOutput(prepared, rendersDir, log) {
15081
15403
  const { input, cleanupProjectDir } = prepared;
15082
15404
  const absoluteOutputPath = resolveOutputPath(input.projectDir, input.outputPath, rendersDir, log);
15083
- const outputDir = dirname12(absoluteOutputPath);
15084
- if (!existsSync19(outputDir)) mkdirSync10(outputDir, { recursive: true });
15405
+ const outputDir = dirname13(absoluteOutputPath);
15406
+ if (!existsSync19(outputDir)) mkdirSync11(outputDir, { recursive: true });
15085
15407
  return { input, cleanupProjectDir, absoluteOutputPath };
15086
15408
  }
15087
15409
  function validateRenderOverrides(body) {
@@ -15121,12 +15443,12 @@ var MAX_PREVIEW_HTML_BYTES = 20 * 1024 * 1024;
15121
15443
  function prepareProjectDirectory(projectDir, options) {
15122
15444
  const candidate = nonEmptyString(projectDir);
15123
15445
  if (!candidate) return null;
15124
- const absProjectDir = resolve9(candidate);
15125
- if (!existsSync19(absProjectDir) || !statSync9(absProjectDir).isDirectory()) {
15446
+ const absProjectDir = resolve10(candidate);
15447
+ if (!existsSync19(absProjectDir) || !statSync10(absProjectDir).isDirectory()) {
15126
15448
  return { error: `Project directory not found: ${absProjectDir}` };
15127
15449
  }
15128
15450
  const entry = options.entryFile || "index.html";
15129
- if (!existsSync19(resolve9(absProjectDir, entry))) {
15451
+ if (!existsSync19(resolve10(absProjectDir, entry))) {
15130
15452
  return { error: `Entry file "${entry}" not found in project directory: ${absProjectDir}` };
15131
15453
  }
15132
15454
  return { prepared: { input: { projectDir: absProjectDir, ...options } } };
@@ -15147,8 +15469,8 @@ async function resolveInlineRenderHtml(body) {
15147
15469
  }
15148
15470
  function materializeInlineProject(html, options) {
15149
15471
  const tempRoot = process.env.PRODUCER_TMP_PROJECT_DIR || tmpdir3();
15150
- const tempProjectDir = mkdtempSync4(join30(tempRoot, "producer-project-"));
15151
- writeFileSync6(join30(tempProjectDir, "index.html"), html, "utf-8");
15472
+ const tempProjectDir = mkdtempSync4(join31(tempRoot, "producer-project-"));
15473
+ writeFileSync7(join31(tempProjectDir, "index.html"), html, "utf-8");
15152
15474
  return {
15153
15475
  prepared: {
15154
15476
  input: { projectDir: tempProjectDir, ...options },
@@ -15173,7 +15495,7 @@ function resolveOutputPath(projectDir, outputCandidate, rendersDir, log) {
15173
15495
  try {
15174
15496
  return resolveRenderPaths(projectDir, outputCandidate, rendersDir).absoluteOutputPath;
15175
15497
  } catch (error) {
15176
- const fallbackPath = resolve9(rendersDir, `producer-fallback-${Date.now()}.mp4`);
15498
+ const fallbackPath = resolve10(rendersDir, `producer-fallback-${Date.now()}.mp4`);
15177
15499
  log.warn("Failed to resolve output path, using fallback", {
15178
15500
  fallback: fallbackPath,
15179
15501
  error: error instanceof Error ? error.message : String(error)
@@ -15209,7 +15531,7 @@ function createArtifactStore(ttlMs) {
15209
15531
  function cleanupTempDir(dir, log) {
15210
15532
  if (!dir) return;
15211
15533
  try {
15212
- rmSync12(dir, { recursive: true, force: true });
15534
+ rmSync13(dir, { recursive: true, force: true });
15213
15535
  } catch (error) {
15214
15536
  log.warn("Failed to cleanup temp project dir", {
15215
15537
  cleanupProjectDir: dir,
@@ -15218,7 +15540,7 @@ function cleanupTempDir(dir, log) {
15218
15540
  }
15219
15541
  }
15220
15542
  function outputFileSize(path) {
15221
- return existsSync19(path) ? statSync9(path).size : 0;
15543
+ return existsSync19(path) ? statSync10(path).size : 0;
15222
15544
  }
15223
15545
  function createBlockingProgressReporter(log, requestId) {
15224
15546
  let lastLoggedPct = -10;
@@ -15524,7 +15846,7 @@ function createRenderHandlers(options = {}) {
15524
15846
  store.delete(token);
15525
15847
  return c.json({ success: false, error: "Output artifact file missing" }, 404);
15526
15848
  }
15527
- const stats = statSync9(artifact.path);
15849
+ const stats = statSync10(artifact.path);
15528
15850
  return new Response(createReadStream3(artifact.path), {
15529
15851
  headers: {
15530
15852
  "content-type": "video/mp4",
@@ -15591,7 +15913,7 @@ function startServer(options = {}) {
15591
15913
  process.on("SIGINT", () => shutdown("SIGINT"));
15592
15914
  return server;
15593
15915
  }
15594
- var entryScript = process.argv[1] ? resolve9(process.argv[1]) : "";
15916
+ var entryScript = process.argv[1] ? resolve10(process.argv[1]) : "";
15595
15917
  var isPublicServerEntry = entryScript.endsWith("/public-server.js") || entryScript.endsWith("/src/server.ts");
15596
15918
  if (isPublicServerEntry) {
15597
15919
  const { values } = parseArgs({