@hyperframes/studio-server 0.7.59 → 0.7.61

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.
Files changed (40) hide show
  1. package/dist/chunk-4ETS2LXI.js +192 -0
  2. package/dist/chunk-4ETS2LXI.js.map +1 -0
  3. package/dist/chunk-6XMC64FJ.js +584 -0
  4. package/dist/chunk-6XMC64FJ.js.map +1 -0
  5. package/dist/chunk-LVXVG4V6.js +282 -0
  6. package/dist/chunk-LVXVG4V6.js.map +1 -0
  7. package/dist/chunk-W2SBTCO2.js +32 -0
  8. package/dist/chunk-W2SBTCO2.js.map +1 -0
  9. package/dist/chunk-X62ASOGO.js +30 -0
  10. package/dist/chunk-X62ASOGO.js.map +1 -0
  11. package/dist/chunk-XVQX2JHE.js +422 -0
  12. package/dist/chunk-XVQX2JHE.js.map +1 -0
  13. package/dist/chunk-YBR7MXIO.js +459 -0
  14. package/dist/chunk-YBR7MXIO.js.map +1 -0
  15. package/dist/chunk-ZUW4PULZ.js +66 -0
  16. package/dist/chunk-ZUW4PULZ.js.map +1 -0
  17. package/dist/helpers/finiteMutation.js +4 -24
  18. package/dist/helpers/finiteMutation.js.map +1 -1
  19. package/dist/helpers/manualEditsRenderScript.js +5 -577
  20. package/dist/helpers/manualEditsRenderScript.js.map +1 -1
  21. package/dist/helpers/mediaCodecMap.d.ts +100 -0
  22. package/dist/helpers/mediaCodecMap.js +25 -0
  23. package/dist/helpers/mediaCodecMap.js.map +1 -0
  24. package/dist/helpers/mediaProxyPreview.d.ts +4 -0
  25. package/dist/helpers/mediaProxyPreview.js +17 -0
  26. package/dist/helpers/mediaProxyPreview.js.map +1 -0
  27. package/dist/helpers/proxyTranscoder.d.ts +61 -0
  28. package/dist/helpers/proxyTranscoder.js +30 -0
  29. package/dist/helpers/proxyTranscoder.js.map +1 -0
  30. package/dist/helpers/screenshotClip.js +3 -22
  31. package/dist/helpers/screenshotClip.js.map +1 -1
  32. package/dist/helpers/sourceMutation.js +9 -419
  33. package/dist/helpers/sourceMutation.js.map +1 -1
  34. package/dist/helpers/studioMotionRenderScript.js +4 -186
  35. package/dist/helpers/studioMotionRenderScript.js.map +1 -1
  36. package/dist/index.d.ts +16 -202
  37. package/dist/index.js +334 -1392
  38. package/dist/index.js.map +1 -1
  39. package/dist/mediaProxyPreview-CeshDUjE.d.ts +259 -0
  40. package/package.json +15 -3
package/dist/index.js CHANGED
@@ -1,3 +1,48 @@
1
+ import {
2
+ findUnsafeDomPatchValues,
3
+ findUnsafeMutationValues
4
+ } from "./chunk-X62ASOGO.js";
5
+ import {
6
+ isHTMLElement,
7
+ patchElementInHtml,
8
+ probeElementInSource,
9
+ removeElementFromHtml,
10
+ splitElementInHtml,
11
+ unwrapElementsFromHtml,
12
+ wrapElementsInHtml
13
+ } from "./chunk-XVQX2JHE.js";
14
+ import {
15
+ getElementScreenshotClip
16
+ } from "./chunk-W2SBTCO2.js";
17
+ import {
18
+ injectMediaCodecMap,
19
+ isAutoProxyEnabled,
20
+ proxyEtagSalt,
21
+ resolvePreviewMediaCodecProbeCache
22
+ } from "./chunk-ZUW4PULZ.js";
23
+ import {
24
+ ProxyCapacityError,
25
+ ProxyTranscodeError,
26
+ resolveProxy
27
+ } from "./chunk-YBR7MXIO.js";
28
+ import {
29
+ PROXY_VARIANT_CONFIG,
30
+ decideMediaProxyEligibility,
31
+ isProxyVariantRequest,
32
+ probeAssetCodec,
33
+ probeMediaMetadata,
34
+ resolveProxyVariantRequest
35
+ } from "./chunk-LVXVG4V6.js";
36
+ import {
37
+ STUDIO_MANUAL_EDITS_PATH,
38
+ createStudioManualEditsRenderBodyScript,
39
+ createStudioPositionSeekReapplyScript
40
+ } from "./chunk-6XMC64FJ.js";
41
+ import {
42
+ STUDIO_MOTION_PATH,
43
+ createStudioMotionRenderBodyScript
44
+ } from "./chunk-4ETS2LXI.js";
45
+
1
46
  // src/createStudioApi.ts
2
47
  import { Hono } from "hono";
3
48
 
@@ -288,9 +333,13 @@ function registerStoryboardRoutes(api, adapter) {
288
333
  // src/routes/files.ts
289
334
  import { bodyLimit } from "hono/body-limit";
290
335
  import {
336
+ closeSync,
291
337
  existsSync as existsSync3,
338
+ ftruncateSync,
339
+ openSync,
292
340
  readFileSync as readFileSync4,
293
341
  writeFileSync as writeFileSync4,
342
+ writeSync,
294
343
  mkdirSync as mkdirSync3,
295
344
  unlinkSync as unlinkSync2,
296
345
  rmSync as rmSync2,
@@ -298,7 +347,7 @@ import {
298
347
  renameSync,
299
348
  readdirSync as readdirSync4
300
349
  } from "fs";
301
- import { resolve as resolve3, dirname, join as join6 } from "path";
350
+ import { resolve as resolve2, dirname, join as join6 } from "path";
302
351
 
303
352
  // src/helpers/mime.ts
304
353
  var MIME_TYPES = {
@@ -315,7 +364,13 @@ var MIME_TYPES = {
315
364
  ".webp": "image/webp",
316
365
  ".ico": "image/x-icon",
317
366
  ".mp4": "video/mp4",
367
+ ".m4v": "video/mp4",
318
368
  ".mov": "video/quicktime",
369
+ ".mkv": "video/x-matroska",
370
+ ".mxf": "video/mxf",
371
+ ".mts": "video/mp2t",
372
+ ".m2ts": "video/mp2t",
373
+ ".ts": "video/mp2t",
319
374
  ".webm": "video/webm",
320
375
  ".mp3": "audio/mpeg",
321
376
  ".wav": "audio/wav",
@@ -343,7 +398,8 @@ function isAudioFile(name) {
343
398
  // src/helpers/waveform.ts
344
399
  import { spawn } from "child_process";
345
400
  import { existsSync as existsSync2, writeFileSync, mkdirSync } from "fs";
346
- import { join as join3, resolve as resolve2 } from "path";
401
+ import { join as join3 } from "path";
402
+ import { findFfBinary } from "@hyperframes/parsers/ff-binaries";
347
403
  var SAMPLE_RATE = 4e3;
348
404
  var PEAK_COUNT = 4e3;
349
405
  var WAVEFORM_CACHE_VERSION = "v2";
@@ -366,15 +422,10 @@ function computePeaks(floats, count) {
366
422
  const maxPeak = Math.max(...peaks, 1e-3);
367
423
  return peaks.map((p) => p / maxPeak);
368
424
  }
369
- function ffmpegBinary() {
370
- const configured = process.env.HYPERFRAMES_FFMPEG_PATH?.trim();
371
- if (configured) return resolve2(configured);
372
- return "ffmpeg";
373
- }
374
425
  function decodeAudioPeaks(audioPath) {
375
426
  return new Promise((resolvePromise, reject) => {
376
427
  const proc = spawn(
377
- ffmpegBinary(),
428
+ findFfBinary("ffmpeg") ?? "ffmpeg",
378
429
  [
379
430
  "-i",
380
431
  audioPath,
@@ -426,7 +477,7 @@ import { spawnSync } from "child_process";
426
477
  import { mkdtempSync, rmSync, writeFileSync as writeFileSync2 } from "fs";
427
478
  import { tmpdir } from "os";
428
479
  import { basename, join as join4 } from "path";
429
- var VIDEO_EXT = /\.(mp4|webm|mov)$/i;
480
+ var VIDEO_EXT = /\.(mp4|webm|mov|mkv|avi|m4v|mxf|mts|m2ts|ts)$/i;
430
481
  var AUDIO_EXT = /\.(mp3|wav|ogg|m4a|aac)$/i;
431
482
  function validateUploadedMedia(filePath, runner = spawnSync) {
432
483
  const isVideo = VIDEO_EXT.test(filePath);
@@ -544,29 +595,36 @@ function pruneBackups(backupDir, backupKey, keepPerFile) {
544
595
  }
545
596
  }
546
597
 
547
- // src/helpers/finiteMutation.ts
548
- function findUnsafeMutationValues(value, path = "body", options = {}) {
549
- if (value === null) {
550
- return options.allowNullPath?.(path) ? [] : [{ path, reason: "null" }];
551
- }
552
- if (typeof value === "number") {
553
- return Number.isFinite(value) ? [] : [{ path, reason: "non-finite-number" }];
554
- }
555
- if (!value || typeof value !== "object") return [];
556
- if (Array.isArray(value)) {
557
- return value.flatMap(
558
- (item, index) => findUnsafeMutationValues(item, `${path}[${index}]`, options)
559
- );
560
- }
561
- return Object.entries(value).flatMap(
562
- ([key, item]) => findUnsafeMutationValues(item, `${path}.${key}`, options)
598
+ // src/helpers/fileVersion.ts
599
+ import { createHash as createHash2, randomUUID } from "crypto";
600
+ var RECEIPT_TTL_MS = 1e4;
601
+ var receipts = /* @__PURE__ */ new Map();
602
+ function fileContentVersion(content) {
603
+ return `"sha256:${createHash2("sha256").update(content, "utf8").digest("hex")}"`;
604
+ }
605
+ function createWriteToken(requestToken) {
606
+ const token = requestToken?.trim();
607
+ return token && token.length <= 200 ? token : randomUUID();
608
+ }
609
+ function recordFileWriteReceipt(absPath, receipt) {
610
+ const now = Date.now();
611
+ const current = (receipts.get(absPath) ?? []).filter(
612
+ (entry) => now - entry.recordedAt < RECEIPT_TTL_MS
563
613
  );
614
+ current.push({ ...receipt, recordedAt: now });
615
+ receipts.set(absPath, current);
564
616
  }
565
- var DOM_PATCH_NULL_VALUE_PATH = /^body\.operations\[\d+\]\.value$/;
566
- function findUnsafeDomPatchValues(value) {
567
- return findUnsafeMutationValues(value, "body", {
568
- allowNullPath: (path) => DOM_PATCH_NULL_VALUE_PATH.test(path)
569
- });
617
+ function consumeFileWriteReceipt(absPath) {
618
+ const now = Date.now();
619
+ const current = (receipts.get(absPath) ?? []).filter(
620
+ (entry) => now - entry.recordedAt < RECEIPT_TTL_MS
621
+ );
622
+ const receipt = current.shift() ?? null;
623
+ if (current.length > 0) receipts.set(absPath, current);
624
+ else receipts.delete(absPath);
625
+ if (!receipt) return null;
626
+ const { path, version, writeToken } = receipt;
627
+ return { path, version, writeToken };
570
628
  }
571
629
 
572
630
  // src/routes/files.ts
@@ -596,429 +654,7 @@ import {
596
654
  scalePositionsInScript,
597
655
  dedupePositionWritesInScript
598
656
  } from "@hyperframes/parsers/gsap-writer-acorn";
599
-
600
- // src/helpers/sourceMutation.ts
601
657
  import { parseHTML } from "linkedom";
602
- import postcss from "postcss";
603
- import selectorParser from "postcss-selector-parser";
604
- import { isAllowedHtmlAttribute, isSafeAttributeValue } from "@hyperframes/core/html-attr-safety";
605
- import { ensureHfIds } from "@hyperframes/parsers/hf-ids";
606
-
607
- // src/helpers/sourceStyleMutation.ts
608
- function parseStyleDecls(style) {
609
- const props = /* @__PURE__ */ new Map();
610
- const order = [];
611
- let i = 0;
612
- while (i < style.length) {
613
- let depth = 0;
614
- let inSingle = false;
615
- let inDouble = false;
616
- const start = i;
617
- while (i < style.length) {
618
- const ch = style[i];
619
- if (ch === "'" && !inDouble) inSingle = !inSingle;
620
- else if (ch === '"' && !inSingle) inDouble = !inDouble;
621
- else if (!inSingle && !inDouble) {
622
- if (ch === "(") depth++;
623
- else if (ch === ")") depth = Math.max(0, depth - 1);
624
- else if (ch === ";" && depth === 0) break;
625
- }
626
- i++;
627
- }
628
- const decl = style.slice(start, i).trim();
629
- i++;
630
- if (!decl) continue;
631
- const colon = decl.indexOf(":");
632
- if (colon < 0) continue;
633
- const key = decl.slice(0, colon).trim();
634
- const val = decl.slice(colon + 1).trim();
635
- if (!key) continue;
636
- if (!props.has(key)) order.push(key);
637
- props.set(key, val);
638
- }
639
- return { props, order };
640
- }
641
- function serializeStyleDecls(props, order) {
642
- return order.map((k) => `${k}: ${props.get(k) ?? ""}`).filter((d) => d.trim()).join("; ");
643
- }
644
- function patchStyleAttrString(style, property, value) {
645
- const { props, order } = parseStyleDecls(style);
646
- if (value === null) {
647
- props.delete(property);
648
- const idx = order.indexOf(property);
649
- if (idx >= 0) order.splice(idx, 1);
650
- } else {
651
- if (!props.has(property)) order.push(property);
652
- props.set(property, value);
653
- }
654
- return serializeStyleDecls(props, order);
655
- }
656
-
657
- // src/helpers/sourceMutation.ts
658
- function parseSourceDocument(source) {
659
- const hasDocumentShell = /<!doctype|<html[\s>]/i.test(source);
660
- if (hasDocumentShell) {
661
- return { document: parseHTML(source).document, wrappedFragment: false };
662
- }
663
- return {
664
- document: parseHTML(`<!DOCTYPE html><html><head></head><body>${source}</body></html>`).document,
665
- wrappedFragment: true
666
- };
667
- }
668
- function duplicateCssRulesForId(document2, originalId, newId) {
669
- const idToken = `#${originalId}`;
670
- const transform = selectorParser((selectors) => {
671
- selectors.walkIds((node) => {
672
- if (node.value === originalId) node.value = newId;
673
- });
674
- });
675
- for (const styleEl of document2.querySelectorAll("style")) {
676
- const css = styleEl.textContent ?? "";
677
- let root;
678
- try {
679
- root = postcss.parse(css);
680
- } catch {
681
- continue;
682
- }
683
- const clones = [];
684
- root.walkRules((rule) => {
685
- if (!rule.selector.includes(idToken)) return;
686
- const newSelector = transform.processSync(rule.selector);
687
- if (newSelector === rule.selector) return;
688
- const clone = rule.clone({ selector: newSelector });
689
- clones.push(clone);
690
- });
691
- if (clones.length > 0) {
692
- for (const c of clones) root.append(c);
693
- styleEl.textContent = root.toString();
694
- }
695
- }
696
- }
697
- function querySelectorAllWithTemplates(root, selector) {
698
- const matches = Array.from(root.querySelectorAll(selector));
699
- if (matches.length > 0) return matches;
700
- const templates = Array.from(root.querySelectorAll("template"));
701
- for (const tmpl of templates) {
702
- const inner = querySelectorAllWithTemplates(tmpl, selector);
703
- if (inner.length > 0) return inner;
704
- }
705
- return [];
706
- }
707
- function escapeCssAttrValue(value) {
708
- return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
709
- }
710
- function findByHfId(document2, hfId) {
711
- try {
712
- const matches = querySelectorAllWithTemplates(
713
- document2,
714
- `[data-hf-id="${escapeCssAttrValue(hfId)}"]`
715
- );
716
- if (matches.length > 1) {
717
- console.warn(
718
- `sourceMutation: data-hf-id "${hfId}" matched ${matches.length} elements; using the first. ids must be unique per document.`
719
- );
720
- }
721
- return matches[0] ?? null;
722
- } catch {
723
- return null;
724
- }
725
- }
726
- function findTargetElement(document2, target) {
727
- if (target.hfId) {
728
- const el = findByHfId(document2, target.hfId);
729
- if (el) return el;
730
- }
731
- if (target.id) {
732
- const byId = document2.getElementById(target.id);
733
- if (byId) return byId;
734
- }
735
- if (!target.selector) return null;
736
- try {
737
- const matches = querySelectorAllWithTemplates(document2, target.selector);
738
- return matches[target.selectorIndex ?? 0] ?? null;
739
- } catch {
740
- return null;
741
- }
742
- }
743
- function removeElementFromHtml(source, target) {
744
- const { document: document2, wrappedFragment } = parseSourceDocument(source);
745
- const element = findTargetElement(document2, target);
746
- if (!element) return source;
747
- element.remove();
748
- return wrappedFragment ? document2.body.innerHTML || "" : document2.toString();
749
- }
750
- function isHTMLElement(el) {
751
- const HTMLEl = el.ownerDocument?.defaultView?.HTMLElement;
752
- return HTMLEl ? el instanceof HTMLEl : el.nodeType === 1 && "style" in el;
753
- }
754
- function resolveOperationTarget(parent, op) {
755
- if (op.childSelector === void 0) return parent;
756
- try {
757
- const child = parent.querySelectorAll(op.childSelector)[op.childIndex ?? 0] ?? null;
758
- return child && isHTMLElement(child) ? child : null;
759
- } catch {
760
- return null;
761
- }
762
- }
763
- function patchElementInHtml(source, target, operations) {
764
- const { document: document2, wrappedFragment } = parseSourceDocument(source);
765
- const el = findTargetElement(document2, target);
766
- if (!el || !isHTMLElement(el)) return { html: source, matched: false };
767
- const htmlEl = el;
768
- const resolved = [];
769
- for (const op of operations) {
770
- const opTarget = resolveOperationTarget(htmlEl, op);
771
- if (!opTarget) return { html: source, matched: false };
772
- resolved.push({ op, target: opTarget });
773
- }
774
- for (const { op, target: opTarget } of resolved) {
775
- switch (op.type) {
776
- case "inline-style":
777
- {
778
- const raw = opTarget.getAttribute("style") ?? "";
779
- const patched = patchStyleAttrString(raw, op.property, op.value);
780
- opTarget.setAttribute("style", patched);
781
- }
782
- break;
783
- case "attribute":
784
- {
785
- const fullAttr = op.property.startsWith("data-") ? op.property : `data-${op.property}`;
786
- if (op.value != null) {
787
- opTarget.setAttribute(fullAttr, op.value);
788
- } else {
789
- opTarget.removeAttribute(fullAttr);
790
- }
791
- }
792
- break;
793
- case "html-attribute":
794
- if (!isAllowedHtmlAttribute(op.property)) break;
795
- if (op.value != null) {
796
- if (!isSafeAttributeValue(op.property, op.value)) break;
797
- opTarget.setAttribute(op.property, op.value);
798
- } else {
799
- opTarget.removeAttribute(op.property);
800
- }
801
- break;
802
- case "text-content":
803
- if (op.value != null) {
804
- const inner = opTarget.children.length === 1 ? opTarget.firstElementChild : null;
805
- const textTarget = inner && isHTMLElement(inner) ? inner : opTarget;
806
- textTarget.textContent = op.value;
807
- }
808
- break;
809
- }
810
- }
811
- return {
812
- html: wrappedFragment ? document2.body.innerHTML || "" : document2.toString(),
813
- matched: true
814
- };
815
- }
816
- function probeElementInSource(source, target) {
817
- if (!target.id && !target.hfId && !target.selector) return false;
818
- const { document: document2 } = parseSourceDocument(source);
819
- const el = findTargetElement(document2, target);
820
- return el != null && isHTMLElement(el);
821
- }
822
- function resolveElementTiming(el) {
823
- const start = parseFloat(el.getAttribute("data-start") ?? "0") || 0;
824
- const usesDataEnd = el.hasAttribute("data-end");
825
- const duration = usesDataEnd ? parseFloat(el.getAttribute("data-end") ?? "") - start || 0 : parseFloat(el.getAttribute("data-duration") ?? "0") || 0;
826
- return { start, duration, usesDataEnd };
827
- }
828
- function setElementDuration(el, start, duration, usesDataEnd) {
829
- if (usesDataEnd) {
830
- const endTime = String(Math.round((start + duration) * 1e3) / 1e3);
831
- el.setAttribute("data-end", endTime);
832
- el.removeAttribute("data-duration");
833
- } else {
834
- el.setAttribute("data-duration", String(Math.round(duration * 1e3) / 1e3));
835
- el.removeAttribute("data-end");
836
- }
837
- }
838
- function splitElementInHtml(source, target, splitTime, newId, fallbackTiming) {
839
- const { document: document2, wrappedFragment } = parseSourceDocument(source);
840
- const el = findTargetElement(document2, target);
841
- if (!el || !isHTMLElement(el)) return { html: source, matched: false, newId: null };
842
- const timing = resolveElementTiming(el);
843
- const { usesDataEnd } = timing;
844
- let { start, duration } = timing;
845
- if (duration <= 0 && fallbackTiming && fallbackTiming.duration > 0) {
846
- start = fallbackTiming.start;
847
- duration = fallbackTiming.duration;
848
- }
849
- if (duration <= 0 || splitTime <= start || splitTime >= start + duration) {
850
- return { html: source, matched: false, newId: null };
851
- }
852
- if (document2.getElementById(newId)) {
853
- let suffix = 2;
854
- const base = newId;
855
- while (document2.getElementById(newId)) {
856
- newId = `${base}-${suffix++}`;
857
- }
858
- }
859
- const firstDuration = splitTime - start;
860
- const secondDuration = duration - firstDuration;
861
- const clone = el.cloneNode(true);
862
- if (!isHTMLElement(clone)) return { html: source, matched: false, newId: null };
863
- clone.setAttribute("id", newId);
864
- clone.removeAttribute("data-hf-id");
865
- for (const node of clone.querySelectorAll("[data-hf-id]")) node.removeAttribute("data-hf-id");
866
- clone.setAttribute("data-start", String(Math.round(splitTime * 1e3) / 1e3));
867
- setElementDuration(clone, splitTime, secondDuration, usesDataEnd);
868
- const playbackStartAttr = el.hasAttribute("data-playback-start") ? "data-playback-start" : el.hasAttribute("data-media-start") ? "data-media-start" : null;
869
- if (playbackStartAttr) {
870
- const currentTrim = parseFloat(el.getAttribute(playbackStartAttr) ?? "0") || 0;
871
- const rateRaw = parseFloat(el.getAttribute("data-playback-rate") ?? "");
872
- const rate = Number.isFinite(rateRaw) ? rateRaw : 1;
873
- clone.setAttribute(
874
- playbackStartAttr,
875
- String(Math.round((currentTrim + firstDuration * rate) * 1e3) / 1e3)
876
- );
877
- }
878
- const originalId = el.getAttribute("id");
879
- if (originalId) {
880
- duplicateCssRulesForId(document2, originalId, newId);
881
- }
882
- el.setAttribute("data-start", String(Math.round(start * 1e3) / 1e3));
883
- setElementDuration(el, start, firstDuration, usesDataEnd);
884
- if (el.nextSibling) {
885
- el.parentElement.insertBefore(clone, el.nextSibling);
886
- } else {
887
- el.parentElement.appendChild(clone);
888
- }
889
- const html = wrappedFragment ? document2.body.innerHTML || "" : document2.toString();
890
- return {
891
- // The split owns its new nodes' stable ids. Leaving the clone unstamped makes
892
- // the next preview request persist different bytes after history is recorded.
893
- html: ensureHfIds(html),
894
- matched: true,
895
- newId
896
- };
897
- }
898
- function getInlineStylePx(el, property) {
899
- const style = (isHTMLElement(el) ? el.getAttribute("style") : null) ?? "";
900
- const { props } = parseStyleDecls(style);
901
- const raw = props.get(property);
902
- if (!raw) return 0;
903
- const n = parseFloat(raw);
904
- return Number.isFinite(n) ? n : 0;
905
- }
906
- function setInlineLeftTop(el, left, top) {
907
- let style = el.getAttribute("style") ?? "";
908
- style = patchStyleAttrString(style, "left", `${left}px`);
909
- style = patchStyleAttrString(style, "top", `${top}px`);
910
- el.setAttribute("style", style);
911
- }
912
- function uniqueGroupDomId(document2, groupId) {
913
- const base = groupId.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "group";
914
- let id = base;
915
- let n = 2;
916
- while (document2.getElementById(id)) {
917
- id = `${base}-${n}`;
918
- n += 1;
919
- }
920
- return id;
921
- }
922
- function wrapElementsInHtml(source, targets, groupId, bbox, rebases) {
923
- const { document: document2, wrappedFragment } = parseSourceDocument(source);
924
- if (targets.length === 0) {
925
- return { html: source, matched: false, groupId: null, error: "no targets" };
926
- }
927
- const els = [];
928
- const seen = /* @__PURE__ */ new Set();
929
- for (const target of targets) {
930
- const el = findTargetElement(document2, target);
931
- if (!el || !isHTMLElement(el) || seen.has(el)) continue;
932
- seen.add(el);
933
- els.push(el);
934
- }
935
- if (els.length === 0) {
936
- return { html: source, matched: false, groupId: null, error: "no targets matched" };
937
- }
938
- const parent = els[0]?.parentElement;
939
- if (!parent || els.some((el) => el.parentElement !== parent)) {
940
- return {
941
- html: source,
942
- matched: false,
943
- groupId: null,
944
- error: "grouped elements must share a single parent"
945
- };
946
- }
947
- const memberSet = new Set(els);
948
- const ordered = Array.from(parent.children).filter((c) => memberSet.has(c));
949
- const rebaseByEl = /* @__PURE__ */ new Map();
950
- for (const rebase of rebases) {
951
- const el = findTargetElement(document2, rebase.target);
952
- if (el) rebaseByEl.set(el, { left: rebase.left, top: rebase.top });
953
- }
954
- const wrapper = document2.createElement("div");
955
- wrapper.setAttribute("data-hf-group", groupId);
956
- wrapper.setAttribute("id", uniqueGroupDomId(document2, groupId));
957
- const memberZIndexes = ordered.map(
958
- (el) => Number.parseInt(
959
- parseStyleDecls(el.getAttribute("style") ?? "").props.get("z-index") ?? "",
960
- 10
961
- )
962
- ).filter((z) => Number.isFinite(z));
963
- const maxZ = memberZIndexes.length > 0 ? Math.max(...memberZIndexes) : null;
964
- wrapper.setAttribute(
965
- "style",
966
- `position: absolute; left: ${bbox.left}px; top: ${bbox.top}px; width: ${bbox.width}px; height: ${bbox.height}px` + (maxZ !== null ? `; z-index: ${maxZ}` : "")
967
- );
968
- parent.insertBefore(wrapper, ordered[ordered.length - 1] ?? null);
969
- for (const el of ordered) {
970
- const rebase = rebaseByEl.get(el);
971
- if (rebase) setInlineLeftTop(el, rebase.left, rebase.top);
972
- wrapper.appendChild(el);
973
- }
974
- return {
975
- html: wrappedFragment ? document2.body.innerHTML || "" : document2.toString(),
976
- matched: true,
977
- groupId
978
- };
979
- }
980
- function unwrapElementsFromHtml(source, groupTarget) {
981
- const { document: document2, wrappedFragment } = parseSourceDocument(source);
982
- const group = findTargetElement(document2, groupTarget);
983
- if (!group || !isHTMLElement(group)) return { html: source, unwrapped: false };
984
- if (!group.hasAttribute("data-hf-group")) return { html: source, unwrapped: false };
985
- const parent = group.parentElement;
986
- if (!parent) return { html: source, unwrapped: false };
987
- const wLeft = getInlineStylePx(group, "left");
988
- const wTop = getInlineStylePx(group, "top");
989
- const groupCenter = {
990
- cx: wLeft + getInlineStylePx(group, "width") / 2,
991
- cy: wTop + getInlineStylePx(group, "height") / 2
992
- };
993
- const members = [];
994
- for (const child of Array.from(group.children)) {
995
- if (isHTMLElement(child)) {
996
- const newLeft = getInlineStylePx(child, "left") + wLeft;
997
- const newTop = getInlineStylePx(child, "top") + wTop;
998
- setInlineLeftTop(child, newLeft, newTop);
999
- if (child.id) {
1000
- members.push({
1001
- id: child.id,
1002
- cx: newLeft + getInlineStylePx(child, "width") / 2,
1003
- cy: newTop + getInlineStylePx(child, "height") / 2
1004
- });
1005
- }
1006
- }
1007
- parent.insertBefore(child, group);
1008
- }
1009
- const groupId = group.id || void 0;
1010
- group.remove();
1011
- return {
1012
- html: wrappedFragment ? document2.body.innerHTML || "" : document2.toString(),
1013
- unwrapped: true,
1014
- unwrappedGroupId: groupId,
1015
- members,
1016
- groupCenter
1017
- };
1018
- }
1019
-
1020
- // src/routes/files.ts
1021
- import { parseHTML as parseHTML2 } from "linkedom";
1022
658
  function isAcornGsapWriterEnabled() {
1023
659
  const val = process.env["STUDIO_SDK_CUTOVER_ENABLED"];
1024
660
  return val === "true" || val === "1";
@@ -1208,7 +844,7 @@ function generateCopyPath(projectDir, originalPath) {
1208
844
  const cleanBase = copyMatch ? base.slice(0, -copyMatch[0].length) : base;
1209
845
  let num = copyMatch ? copyMatch[1] ? parseInt(copyMatch[1]) + 1 : 2 : 1;
1210
846
  let candidate = num === 1 ? `${cleanBase} (copy)${ext}` : `${cleanBase} (copy ${num})${ext}`;
1211
- while (existsSync3(resolve3(projectDir, candidate))) {
847
+ while (existsSync3(resolve2(projectDir, candidate))) {
1212
848
  num++;
1213
849
  candidate = `${cleanBase} (copy ${num})${ext}`;
1214
850
  }
@@ -1219,7 +855,7 @@ function walkFiles(dir, filter) {
1219
855
  for (const entry of readdirSync4(dir, { withFileTypes: true })) {
1220
856
  const full = join6(dir, entry.name);
1221
857
  if (entry.isDirectory()) {
1222
- if (entry.name === "node_modules" || entry.name === ".thumbnails" || entry.name === "renders")
858
+ if (entry.name === "node_modules" || entry.name === ".thumbnails" || entry.name === "renders" || entry.name === ".transcode-cache")
1223
859
  continue;
1224
860
  results.push(...walkFiles(full, filter));
1225
861
  } else if (filter(entry.name)) {
@@ -1246,10 +882,10 @@ function updateReferences(projectDir, oldPath, newPath) {
1246
882
  return updatedCount;
1247
883
  }
1248
884
  function extractGsapScriptBlock(html) {
1249
- const { document: document2 } = parseHTML2(html);
885
+ const { document } = parseHTML(html);
1250
886
  const scripts = [
1251
- ...document2.querySelectorAll("script:not([src])"),
1252
- ...Array.from(document2.querySelectorAll("template")).flatMap(
887
+ ...document.querySelectorAll("script:not([src])"),
888
+ ...Array.from(document.querySelectorAll("template")).flatMap(
1253
889
  (tmpl) => Array.from(tmpl.querySelectorAll("script:not([src])"))
1254
890
  )
1255
891
  ];
@@ -1258,10 +894,10 @@ function extractGsapScriptBlock(html) {
1258
894
  if (content.includes("gsap.timeline") || content.includes(".set(") || content.includes(".to(")) {
1259
895
  return {
1260
896
  scriptText: content,
1261
- document: document2,
897
+ document,
1262
898
  replaceScript(newText) {
1263
899
  script.textContent = newText;
1264
- return document2.toString();
900
+ return document.toString();
1265
901
  }
1266
902
  };
1267
903
  }
@@ -1359,11 +995,11 @@ function bakeGroupTransformIntoMembers(html, groupId, members, groupCenter) {
1359
995
  }
1360
996
  return block.replaceScript(script);
1361
997
  }
1362
- function stripStudioEditsFromTarget(document2, selector) {
998
+ function stripStudioEditsFromTarget(document, selector) {
1363
999
  if (!selector) return 0;
1364
1000
  let stripped = 0;
1365
1001
  try {
1366
- for (const el of document2.querySelectorAll(selector)) {
1002
+ for (const el of document.querySelectorAll(selector)) {
1367
1003
  if (!isHTMLElement(el)) continue;
1368
1004
  const htmlEl = el;
1369
1005
  let touched = false;
@@ -1433,11 +1069,11 @@ function resolveFinalOpacity(anim) {
1433
1069
  const num = Number(raw);
1434
1070
  return Number.isFinite(num) && num !== 0 ? num : null;
1435
1071
  }
1436
- function bakeVisibilityOnDelete(document2, anim) {
1072
+ function bakeVisibilityOnDelete(document, anim) {
1437
1073
  const opacity = resolveFinalOpacity(anim);
1438
1074
  if (opacity === null) return;
1439
1075
  try {
1440
- for (const el of document2.querySelectorAll(anim.targetSelector)) {
1076
+ for (const el of document.querySelectorAll(anim.targetSelector)) {
1441
1077
  if (isHTMLElement(el)) el.style.setProperty("opacity", String(opacity));
1442
1078
  }
1443
1079
  } catch {
@@ -1570,9 +1206,11 @@ async function applyGsapMutations(c, res, mutations) {
1570
1206
  after: newHtml,
1571
1207
  scriptText: block.scriptText,
1572
1208
  path: res.filePath,
1209
+ version: fileContentVersion(newHtml),
1573
1210
  backupPath
1574
1211
  };
1575
1212
  if (skippedSelectors.size > 0) responsePayload.skippedSelectors = [...skippedSelectors];
1213
+ c.header("ETag", responsePayload.version);
1576
1214
  return c.json(responsePayload);
1577
1215
  }
1578
1216
  function executeGsapMutationAcorn(body, block, respond) {
@@ -2188,7 +1826,7 @@ async function processUploadedFiles(formData, targetDir, projectDir) {
2188
1826
  skipped.push(name);
2189
1827
  continue;
2190
1828
  }
2191
- const destPath = resolve3(targetDir, name);
1829
+ const destPath = resolve2(targetDir, name);
2192
1830
  if (!isSafePath(projectDir, destPath)) continue;
2193
1831
  let finalPath = destPath;
2194
1832
  let finalName = name;
@@ -2198,13 +1836,13 @@ async function processUploadedFiles(formData, targetDir, projectDir) {
2198
1836
  const base = dotIdx > 0 ? name.slice(0, dotIdx) : name;
2199
1837
  let n = 2;
2200
1838
  const MAX_COPY_INDEX = 1e4;
2201
- while (n < MAX_COPY_INDEX && existsSync3(resolve3(targetDir, `${base} (${n})${ext}`))) n++;
1839
+ while (n < MAX_COPY_INDEX && existsSync3(resolve2(targetDir, `${base} (${n})${ext}`))) n++;
2202
1840
  if (n >= MAX_COPY_INDEX) {
2203
1841
  skipped.push(name);
2204
1842
  continue;
2205
1843
  }
2206
1844
  finalName = `${base} (${n})${ext}`;
2207
- finalPath = resolve3(targetDir, finalName);
1845
+ finalPath = resolve2(targetDir, finalName);
2208
1846
  }
2209
1847
  const buffer = Buffer.from(await value.arrayBuffer());
2210
1848
  const validation = validateUploadedMediaBuffer(finalName, buffer);
@@ -2233,19 +1871,111 @@ function registerFileRoutes(api, adapter) {
2233
1871
  return c.json({ error: "not found" }, 404);
2234
1872
  }
2235
1873
  const content = readFileSync4(res.absPath, "utf-8");
2236
- return c.json({ filename: res.filePath, content });
1874
+ const version = fileContentVersion(content);
1875
+ c.header("ETag", version);
1876
+ return c.json({ filename: res.filePath, content, version });
2237
1877
  });
2238
1878
  api.put("/projects/:id/files/*", async (c) => {
2239
1879
  const res = await resolveProjectFile(c, adapter);
2240
1880
  if ("error" in res) return res.error;
2241
- ensureDir(res.absPath);
2242
1881
  const body = await c.req.text();
2243
- const backup = snapshotBeforeWrite(res.project.dir, res.absPath);
2244
- if (backup.error) console.warn(`Failed to create backup for ${res.filePath}: ${backup.error}`);
2245
- writeFileSync4(res.absPath, body, "utf-8");
1882
+ const expectedVersion = c.req.header("If-Match")?.trim() ?? null;
1883
+ const createOnly = c.req.header("If-None-Match")?.trim() === "*";
1884
+ if (expectedVersion === null && !createOnly) {
1885
+ let currentContent = null;
1886
+ try {
1887
+ currentContent = readFileSync4(res.absPath, "utf-8");
1888
+ } catch (error) {
1889
+ if (!error || typeof error !== "object" || !("code" in error) || error.code !== "ENOENT") {
1890
+ throw error;
1891
+ }
1892
+ }
1893
+ return c.json(
1894
+ {
1895
+ error: "precondition required",
1896
+ path: res.filePath,
1897
+ currentVersion: currentContent === null ? null : fileContentVersion(currentContent),
1898
+ currentContent
1899
+ },
1900
+ 428
1901
+ );
1902
+ }
1903
+ let backup = { backupPath: null };
1904
+ if (createOnly) {
1905
+ ensureDir(res.absPath);
1906
+ let fd;
1907
+ try {
1908
+ fd = openSync(res.absPath, "wx");
1909
+ } catch (error) {
1910
+ if (!error || typeof error !== "object" || !("code" in error) || error.code !== "EEXIST") {
1911
+ throw error;
1912
+ }
1913
+ const currentContent = readFileSync4(res.absPath, "utf-8");
1914
+ return c.json(
1915
+ {
1916
+ error: "file conflict",
1917
+ path: res.filePath,
1918
+ currentVersion: fileContentVersion(currentContent),
1919
+ currentContent
1920
+ },
1921
+ 409
1922
+ );
1923
+ }
1924
+ try {
1925
+ writeSync(fd, body, 0, "utf-8");
1926
+ } finally {
1927
+ closeSync(fd);
1928
+ }
1929
+ } else {
1930
+ let fd;
1931
+ try {
1932
+ fd = openSync(res.absPath, "r+");
1933
+ } catch (error) {
1934
+ if (!error || typeof error !== "object" || !("code" in error) || error.code !== "ENOENT") {
1935
+ throw error;
1936
+ }
1937
+ return c.json(
1938
+ {
1939
+ error: "file conflict",
1940
+ path: res.filePath,
1941
+ currentVersion: null,
1942
+ currentContent: null
1943
+ },
1944
+ 409
1945
+ );
1946
+ }
1947
+ try {
1948
+ const currentContent = readFileSync4(fd, "utf-8");
1949
+ const currentVersion = fileContentVersion(currentContent);
1950
+ if (expectedVersion !== currentVersion) {
1951
+ return c.json(
1952
+ {
1953
+ error: "file conflict",
1954
+ path: res.filePath,
1955
+ currentVersion,
1956
+ currentContent
1957
+ },
1958
+ 409
1959
+ );
1960
+ }
1961
+ backup = snapshotBeforeWrite(res.project.dir, res.absPath);
1962
+ if (backup.error)
1963
+ console.warn(`Failed to create backup for ${res.filePath}: ${backup.error}`);
1964
+ ftruncateSync(fd, 0);
1965
+ writeSync(fd, body, 0, "utf-8");
1966
+ } finally {
1967
+ closeSync(fd);
1968
+ }
1969
+ }
1970
+ const version = fileContentVersion(body);
1971
+ const writeToken = createWriteToken(c.req.header("X-Hyperframes-Write-Token"));
1972
+ recordFileWriteReceipt(res.absPath, { path: res.filePath, version, writeToken });
1973
+ c.header("ETag", version);
2246
1974
  return c.json({
2247
1975
  ok: true,
2248
1976
  path: res.filePath,
1977
+ version,
1978
+ writeToken,
2249
1979
  backupPath: backupPathForResponse(res.project.dir, backup.backupPath)
2250
1980
  });
2251
1981
  });
@@ -2317,17 +2047,28 @@ function registerFileRoutes(api, adapter) {
2317
2047
  fallbackTiming
2318
2048
  );
2319
2049
  if (!result.matched) {
2320
- return c.json({ ok: false, changed: false, content: originalContent, path: ctx.filePath });
2050
+ const version2 = fileContentVersion(originalContent);
2051
+ c.header("ETag", version2);
2052
+ return c.json({
2053
+ ok: false,
2054
+ changed: false,
2055
+ content: originalContent,
2056
+ path: ctx.filePath,
2057
+ version: version2
2058
+ });
2321
2059
  }
2322
2060
  const backup = snapshotBeforeWrite(ctx.project.dir, ctx.absPath);
2323
2061
  if (backup.error) console.warn(`Failed to create backup for ${ctx.filePath}: ${backup.error}`);
2324
2062
  writeFileSync4(ctx.absPath, result.html, "utf-8");
2063
+ const version = fileContentVersion(result.html);
2064
+ c.header("ETag", version);
2325
2065
  return c.json({
2326
2066
  ok: true,
2327
2067
  changed: true,
2328
2068
  content: result.html,
2329
2069
  newId: result.newId,
2330
2070
  path: ctx.filePath,
2071
+ version,
2331
2072
  backupPath: backupPathForResponse(ctx.project.dir, backup.backupPath)
2332
2073
  });
2333
2074
  });
@@ -2650,13 +2391,13 @@ function registerFileRoutes(api, adapter) {
2650
2391
  // src/routes/preview.ts
2651
2392
  import { existsSync as existsSync5, readFileSync as readFileSync7, statSync as statSync2 } from "fs";
2652
2393
  import { join as join8 } from "path";
2653
- import { createHash as createHash2 } from "crypto";
2394
+ import { createHash as createHash3 } from "crypto";
2654
2395
  import { injectScriptsIntoHtml, stripEmbeddedRuntimeScripts as stripEmbeddedRuntimeScripts2 } from "@hyperframes/core/compiler";
2655
2396
 
2656
2397
  // src/helpers/subComposition.ts
2657
2398
  import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
2658
2399
  import { join as join7 } from "path";
2659
- import { parseHTML as parseHTML3 } from "linkedom";
2400
+ import { parseHTML as parseHTML2 } from "linkedom";
2660
2401
  import {
2661
2402
  rewriteAssetPaths,
2662
2403
  rewriteCssAssetUrls,
@@ -2704,7 +2445,7 @@ function fixDigitLeadingIdSelectors(root) {
2704
2445
  }
2705
2446
  }
2706
2447
  function extractFullDocumentParts(rawHtml, compPath) {
2707
- const { document: doc } = parseHTML3(rawHtml);
2448
+ const { document: doc } = parseHTML2(rawHtml);
2708
2449
  const rewriteTargets = [doc.head, doc.body].filter(Boolean);
2709
2450
  for (const target of rewriteTargets) {
2710
2451
  rewriteRelativePaths(target, compPath);
@@ -2718,7 +2459,7 @@ function extractFullDocumentParts(rawHtml, compPath) {
2718
2459
  return { headContent, bodyContent, htmlAttrs, bodyAttrs };
2719
2460
  }
2720
2461
  function extractTemplateInnerHtml(rawComp) {
2721
- const { document: doc } = parseHTML3(rawComp);
2462
+ const { document: doc } = parseHTML2(rawComp);
2722
2463
  const template = doc.querySelector("template");
2723
2464
  return template ? template.innerHTML : null;
2724
2465
  }
@@ -2770,7 +2511,7 @@ function buildSubCompositionHtml(projectDir, compPath, runtimeUrl, baseHref, raw
2770
2511
  let bodyAttrs = "";
2771
2512
  const templateInner = extractTemplateInnerHtml(rawComp);
2772
2513
  if (templateInner != null) {
2773
- const { document: contentDoc } = parseHTML3(
2514
+ const { document: contentDoc } = parseHTML2(
2774
2515
  `<!DOCTYPE html><html><head></head><body>${templateInner}</body></html>`
2775
2516
  );
2776
2517
  rewriteRelativePaths(contentDoc, compPath);
@@ -2784,7 +2525,7 @@ function buildSubCompositionHtml(projectDir, compPath, runtimeUrl, baseHref, raw
2784
2525
  htmlAttrs = parts.htmlAttrs;
2785
2526
  bodyAttrs = parts.bodyAttrs;
2786
2527
  } else {
2787
- const { document: contentDoc } = parseHTML3(
2528
+ const { document: contentDoc } = parseHTML2(
2788
2529
  `<!DOCTYPE html><html><head></head><body>${rawComp}</body></html>`
2789
2530
  );
2790
2531
  rewriteRelativePaths(contentDoc, compPath);
@@ -2829,210 +2570,23 @@ ${rewrittenContent}
2829
2570
  </html>`;
2830
2571
  }
2831
2572
 
2832
- // src/helpers/studioMotionRenderScript.ts
2833
- var STUDIO_MOTION_PATH = ".hyperframes/studio-motion.json";
2834
- function hasStudioMotionEntries(manifestContent) {
2835
- try {
2836
- const parsed = JSON.parse(manifestContent);
2837
- return Array.isArray(parsed.motions) && parsed.motions.length > 0;
2838
- } catch {
2839
- return false;
2840
- }
2841
- }
2842
- function createStudioMotionRenderBodyScript(manifestContent, options = {}) {
2843
- if (!manifestContent.trim() || !hasStudioMotionEntries(manifestContent)) return null;
2844
- return `(${studioMotionRenderRuntime.toString()})(${JSON.stringify(manifestContent)}, ${JSON.stringify(options.activeCompositionPath ?? null)});`;
2845
- }
2846
- function studioMotionRenderRuntime(manifestContent, activeCompositionPath) {
2847
- const STUDIO_MOTION_TIMELINE_ID = "studio-motion";
2848
- const STUDIO_MOTION_ATTR = "data-hf-studio-motion";
2849
- const ORIGINAL_TRANSFORM_ATTR = "data-hf-studio-motion-original-transform";
2850
- const ORIGINAL_OPACITY_ATTR = "data-hf-studio-motion-original-opacity";
2851
- const ORIGINAL_VISIBILITY_ATTR = "data-hf-studio-motion-original-visibility";
2852
- const objectRecord = (value) => value && typeof value === "object" ? value : null;
2853
- const finiteNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
2854
- const runtimeWindow = window;
2855
- const parseMotionValues = (value) => {
2856
- const record = objectRecord(value);
2857
- if (!record) return null;
2858
- const parsed = {};
2859
- for (const key of ["x", "y", "scale", "rotation", "opacity", "autoAlpha"]) {
2860
- const next = finiteNumber(record[key]);
2861
- if (next != null) parsed[key] = next;
2862
- }
2863
- return Object.keys(parsed).length > 0 ? parsed : null;
2864
- };
2865
- const parseCustomEase = (value) => {
2866
- const record = objectRecord(value);
2867
- if (!record) return null;
2868
- const id = typeof record.id === "string" ? record.id.trim() : "";
2869
- const data = typeof record.data === "string" ? record.data.trim() : "";
2870
- if (!id || !data) return null;
2871
- return { id, data };
2872
- };
2873
- const parsedManifest = (() => {
2874
- try {
2875
- return objectRecord(JSON.parse(manifestContent));
2876
- } catch {
2877
- return null;
2878
- }
2879
- })();
2880
- const manifestMotions = Array.isArray(parsedManifest?.motions) ? parsedManifest.motions : [];
2881
- const sourceFileForElement = (element) => {
2882
- let current = element;
2883
- while (current) {
2884
- const sourceFile = current.getAttribute("data-composition-file") ?? current.getAttribute("data-composition-src");
2885
- if (sourceFile) return sourceFile;
2886
- current = current.parentElement;
2887
- }
2888
- return activeCompositionPath ?? "index.html";
2889
- };
2890
- const elementMatchesSourceFile = (element, sourceFile) => sourceFileForElement(element) === sourceFile;
2891
- const isHTMLElement2 = (element) => element instanceof HTMLElement;
2892
- const querySelectorCandidates = (selector) => {
2893
- const className = selector.match(/^\.([A-Za-z0-9_-]+)$/)?.[1];
2894
- if (className) {
2895
- return Array.from(document.getElementsByTagName("*")).filter(
2896
- (element) => isHTMLElement2(element) && element.classList.contains(className)
2897
- );
2898
- }
2899
- if (/^[A-Za-z][A-Za-z0-9-]*$/.test(selector)) {
2900
- return Array.from(document.getElementsByTagName(selector)).filter(isHTMLElement2);
2901
- }
2902
- return Array.from(document.querySelectorAll(selector)).filter(isHTMLElement2);
2903
- };
2904
- const resolveTarget = (targetRecord) => {
2905
- const sourceFile = typeof targetRecord.sourceFile === "string" ? targetRecord.sourceFile : "";
2906
- if (!sourceFile) return null;
2907
- const id = typeof targetRecord.id === "string" ? targetRecord.id : "";
2908
- if (id) {
2909
- const byId = document.getElementById(id);
2910
- if (isHTMLElement2(byId) && elementMatchesSourceFile(byId, sourceFile)) return byId;
2911
- }
2912
- const selector = typeof targetRecord.selector === "string" ? targetRecord.selector : "";
2913
- if (!selector) return null;
2914
- try {
2915
- const selectorIndex = Math.max(0, Math.floor(finiteNumber(targetRecord.selectorIndex) ?? 0));
2916
- return querySelectorCandidates(selector).filter(
2917
- (element) => elementMatchesSourceFile(element, sourceFile)
2918
- )[selectorIndex] ?? null;
2919
- } catch {
2920
- return null;
2921
- }
2922
- };
2923
- const restoreElement = (element) => {
2924
- runtimeWindow.gsap?.set?.(element, { clearProps: "transform,opacity,visibility" });
2925
- element.style.transform = element.getAttribute(ORIGINAL_TRANSFORM_ATTR) ?? "";
2926
- element.style.opacity = element.getAttribute(ORIGINAL_OPACITY_ATTR) ?? "";
2927
- element.style.visibility = element.getAttribute(ORIGINAL_VISIBILITY_ATTR) ?? "";
2928
- element.removeAttribute(STUDIO_MOTION_ATTR);
2929
- element.removeAttribute(ORIGINAL_TRANSFORM_ATTR);
2930
- element.removeAttribute(ORIGINAL_OPACITY_ATTR);
2931
- element.removeAttribute(ORIGINAL_VISIBILITY_ATTR);
2932
- };
2933
- const restoreStudioMotionElements = () => {
2934
- for (const element of Array.from(document.querySelectorAll(`[${STUDIO_MOTION_ATTR}]`))) {
2935
- if (isHTMLElement2(element)) restoreElement(element);
2936
- }
2937
- };
2938
- const readCurrentTime = () => {
2939
- try {
2940
- const playerTime = runtimeWindow.__player?.getTime?.();
2941
- if (typeof playerTime === "number" && Number.isFinite(playerTime)) {
2942
- return Math.max(0, playerTime);
2943
- }
2944
- } catch {
2945
- }
2946
- try {
2947
- const timelineTime = runtimeWindow.__timeline?.time?.();
2948
- if (typeof timelineTime === "number" && Number.isFinite(timelineTime)) {
2949
- return Math.max(0, timelineTime);
2950
- }
2951
- } catch {
2952
- }
2953
- return 0;
2954
- };
2955
- const resolveEase = (motion) => {
2956
- const fallback = typeof motion.ease === "string" && motion.ease.trim() ? motion.ease.trim() : "none";
2957
- const customEase = parseCustomEase(motion.customEase);
2958
- const customEasePlugin = runtimeWindow.CustomEase;
2959
- if (!customEase || typeof customEasePlugin?.create !== "function") return fallback;
2960
- try {
2961
- runtimeWindow.gsap?.registerPlugin?.(customEasePlugin);
2962
- customEasePlugin.create(customEase.id, customEase.data);
2963
- return customEase.id;
2964
- } catch {
2965
- return fallback;
2966
- }
2967
- };
2968
- const applyManifest = () => {
2969
- runtimeWindow.__timelines = runtimeWindow.__timelines ?? {};
2970
- runtimeWindow.__timelines[STUDIO_MOTION_TIMELINE_ID]?.kill?.();
2971
- delete runtimeWindow.__timelines[STUDIO_MOTION_TIMELINE_ID];
2972
- restoreStudioMotionElements();
2973
- const gsap = runtimeWindow.gsap;
2974
- if (!gsap?.timeline || manifestMotions.length === 0) return 0;
2975
- const timeline = gsap.timeline({ paused: true, defaults: { overwrite: "auto" } });
2976
- let applied = 0;
2977
- for (const motionValue of manifestMotions) {
2978
- const motion = objectRecord(motionValue);
2979
- if (!motion || motion.kind !== "gsap-motion") continue;
2980
- const targetRecord = objectRecord(motion.target);
2981
- if (!targetRecord) continue;
2982
- const target = resolveTarget(targetRecord);
2983
- if (!target || typeof timeline.fromTo !== "function") continue;
2984
- const start = finiteNumber(motion.start);
2985
- const duration = finiteNumber(motion.duration);
2986
- if (start == null || duration == null || start < 0 || duration <= 0) continue;
2987
- const from = parseMotionValues(motion.from);
2988
- const to = parseMotionValues(motion.to);
2989
- if (!from || !to) continue;
2990
- if (!target.hasAttribute(STUDIO_MOTION_ATTR)) {
2991
- target.setAttribute(ORIGINAL_TRANSFORM_ATTR, target.style.transform);
2992
- target.setAttribute(ORIGINAL_OPACITY_ATTR, target.style.opacity);
2993
- target.setAttribute(ORIGINAL_VISIBILITY_ATTR, target.style.visibility);
2994
- }
2995
- target.setAttribute(STUDIO_MOTION_ATTR, "true");
2996
- timeline.fromTo(
2997
- target,
2998
- from,
2999
- { ...to, duration, ease: resolveEase(motion), overwrite: "auto", immediateRender: false },
3000
- start
3001
- );
3002
- applied += 1;
3003
- }
3004
- if (applied === 0) {
3005
- timeline.kill?.();
3006
- return 0;
3007
- }
3008
- runtimeWindow.__timelines[STUDIO_MOTION_TIMELINE_ID] = timeline;
3009
- timeline.pause?.();
3010
- const currentTime = readCurrentTime();
3011
- if (typeof timeline.totalTime === "function") timeline.totalTime(currentTime, false);
3012
- else timeline.time?.(currentTime);
3013
- return applied;
3014
- };
3015
- runtimeWindow.__hfStudioMotionApply = applyManifest;
3016
- applyManifest();
3017
- }
3018
-
3019
2573
  // src/routes/preview.ts
3020
- import { ensureHfIds as ensureHfIds3 } from "@hyperframes/parsers/hf-ids";
2574
+ import { ensureHfIds as ensureHfIds2 } from "@hyperframes/parsers/hf-ids";
3021
2575
 
3022
2576
  // src/helpers/hfIdPersist.ts
3023
- import { ensureHfIds as ensureHfIds2 } from "@hyperframes/parsers/hf-ids";
2577
+ import { ensureHfIds } from "@hyperframes/parsers/hf-ids";
3024
2578
  import {
3025
- closeSync,
2579
+ closeSync as closeSync2,
3026
2580
  constants,
3027
2581
  fstatSync,
3028
- ftruncateSync,
3029
- openSync,
2582
+ ftruncateSync as ftruncateSync2,
2583
+ openSync as openSync2,
3030
2584
  readFileSync as readFileSync6,
3031
2585
  writeFileSync as writeFileSync5,
3032
- writeSync
2586
+ writeSync as writeSync2
3033
2587
  } from "fs";
3034
2588
  function persistHfIdsIfNeeded(filePath, html) {
3035
- const normalized = ensureHfIds2(html);
2589
+ const normalized = ensureHfIds(html);
3036
2590
  const idsBefore = (html.match(/\bdata-hf-id=/g) ?? []).length;
3037
2591
  const idsAfter = (normalized.match(/\bdata-hf-id=/g) ?? []).length;
3038
2592
  if (idsAfter > idsBefore) {
@@ -3050,7 +2604,7 @@ function persistHfIdsIfNeeded(filePath, html) {
3050
2604
  function openNoFollow(filePath, flags) {
3051
2605
  const noFollow = constants.O_NOFOLLOW ?? 0;
3052
2606
  try {
3053
- return openSync(filePath, flags | noFollow);
2607
+ return openSync2(filePath, flags | noFollow);
3054
2608
  } catch {
3055
2609
  return null;
3056
2610
  }
@@ -3066,19 +2620,19 @@ function stampFileHfIds(filePath) {
3066
2620
  try {
3067
2621
  if (!fstatSync(fd).isFile()) return null;
3068
2622
  const html = readFileSync6(fd, "utf-8");
3069
- const normalized = ensureHfIds2(html);
2623
+ const normalized = ensureHfIds(html);
3070
2624
  const idsBefore = (html.match(/\bdata-hf-id=/g) ?? []).length;
3071
2625
  const idsAfter = (normalized.match(/\bdata-hf-id=/g) ?? []).length;
3072
2626
  if (writable && idsAfter > idsBefore) {
3073
- ftruncateSync(fd, 0);
3074
- writeSync(fd, normalized, 0, "utf-8");
2627
+ ftruncateSync2(fd, 0);
2628
+ writeSync2(fd, normalized, 0, "utf-8");
3075
2629
  }
3076
2630
  return normalized;
3077
2631
  } catch (err) {
3078
2632
  console.warn("[hyperframes] stampFileHfIds: failed to stamp ids:", err);
3079
2633
  return null;
3080
2634
  } finally {
3081
- closeSync(fd);
2635
+ closeSync2(fd);
3082
2636
  }
3083
2637
  }
3084
2638
 
@@ -3234,7 +2788,7 @@ function parsePreviewVariablesParam(raw) {
3234
2788
  }
3235
2789
  function variablesEtagSalt(raw) {
3236
2790
  if (!raw) return "";
3237
- return `:vars:${createHash2("sha1").update(raw).digest("hex").slice(0, 12)}`;
2791
+ return `:vars:${createHash3("sha1").update(raw).digest("hex").slice(0, 12)}`;
3238
2792
  }
3239
2793
  function previewVariablesFromRequest(rawVariables) {
3240
2794
  const parse = parsePreviewVariablesParam(rawVariables);
@@ -3268,11 +2822,17 @@ async function transformPreviewHtml(html, adapter, project, activeCompositionPat
3268
2822
  function resolveProjectMainHtml(projectDir, projectId) {
3269
2823
  const indexPath = join8(projectDir, "index.html");
3270
2824
  if (existsSync5(indexPath)) {
3271
- return { html: readFileSync7(indexPath, "utf-8"), compositionPath: "index.html" };
2825
+ return {
2826
+ html: readFileSync7(indexPath, "utf-8"),
2827
+ compositionPath: "index.html"
2828
+ };
3272
2829
  }
3273
2830
  const blockHtmlPath = join8(projectDir, `${projectId}.html`);
3274
2831
  if (existsSync5(blockHtmlPath)) {
3275
- return { html: readFileSync7(blockHtmlPath, "utf-8"), compositionPath: `${projectId}.html` };
2832
+ return {
2833
+ html: readFileSync7(blockHtmlPath, "utf-8"),
2834
+ compositionPath: `${projectId}.html`
2835
+ };
3276
2836
  }
3277
2837
  return null;
3278
2838
  }
@@ -3281,6 +2841,7 @@ function registerPreviewRoutes(api, adapter) {
3281
2841
  "Cache-Control": "private, no-cache",
3282
2842
  ETag: etag
3283
2843
  });
2844
+ const mediaCodecProbeCache = resolvePreviewMediaCodecProbeCache(adapter);
3284
2845
  api.get("/projects/:id/preview", async (c) => {
3285
2846
  const resolved = await resolveProjectAndSignature(adapter, c.req.param("id"));
3286
2847
  if (!resolved) return c.json({ error: "not found" }, 404);
@@ -3291,7 +2852,10 @@ function registerPreviewRoutes(api, adapter) {
3291
2852
  const etag = `"preview:${signature}${variablesEtagSalt(vars.raw)}"`;
3292
2853
  const ifNoneMatch = c.req.header("If-None-Match");
3293
2854
  if (ifNoneMatch === etag) {
3294
- return new Response(null, { status: 304, headers: previewCacheHeaders(etag) });
2855
+ return new Response(null, {
2856
+ status: 304,
2857
+ headers: previewCacheHeaders(etag)
2858
+ });
3295
2859
  }
3296
2860
  const diskMain = resolveProjectMainHtml(project.dir, project.id);
3297
2861
  const normalizedDisk = diskMain ? persistHfIdsIfNeeded(join8(project.dir, diskMain.compositionPath), diskMain.html) : null;
@@ -3314,12 +2878,19 @@ ${runtimeTag}`;
3314
2878
  bundled = bundled.replace(/<head>/i, `<head><base href="${baseHref}">`);
3315
2879
  }
3316
2880
  bundled = injectStudioPreviewAugmentations(
3317
- ensureHfIds3(await transformPreviewHtml(bundled, adapter, project, mainCompositionPath)),
2881
+ ensureHfIds2(await transformPreviewHtml(bundled, adapter, project, mainCompositionPath)),
3318
2882
  adapter,
3319
2883
  project.dir,
3320
2884
  mainCompositionPath
3321
2885
  );
3322
2886
  if (previewVariables) bundled = injectPreviewVariables(bundled, previewVariables);
2887
+ bundled = await injectMediaCodecMap(
2888
+ bundled,
2889
+ adapter,
2890
+ project.dir,
2891
+ mainCompositionPath,
2892
+ mediaCodecProbeCache
2893
+ );
3323
2894
  return c.html(bundled, 200, previewCacheHeaders(etag));
3324
2895
  } catch {
3325
2896
  const fallback = resolveProjectMainHtml(project.dir, project.id);
@@ -3337,6 +2908,13 @@ ${runtimeTag}`;
3337
2908
  if (previewVariables) {
3338
2909
  fallbackAugmented = injectPreviewVariables(fallbackAugmented, previewVariables);
3339
2910
  }
2911
+ fallbackAugmented = await injectMediaCodecMap(
2912
+ fallbackAugmented,
2913
+ adapter,
2914
+ project.dir,
2915
+ fallback.compositionPath,
2916
+ mediaCodecProbeCache
2917
+ );
3340
2918
  return c.html(fallbackAugmented, 200, previewCacheHeaders(etag));
3341
2919
  }
3342
2920
  return c.text("not found", 404);
@@ -3363,7 +2941,10 @@ ${runtimeTag}`;
3363
2941
  const etag = `"comp:v2:${compPath}:${signature}${variablesEtagSalt(vars.raw)}"`;
3364
2942
  const ifNoneMatch = c.req.header("If-None-Match");
3365
2943
  if (ifNoneMatch === etag) {
3366
- return new Response(null, { status: 304, headers: previewCacheHeaders(etag) });
2944
+ return new Response(null, {
2945
+ status: 304,
2946
+ headers: previewCacheHeaders(etag)
2947
+ });
3367
2948
  }
3368
2949
  const stamped = pinSubCompHfIds(compFile, compPath);
3369
2950
  if (stamped === null) return c.text("not found", 404);
@@ -3376,9 +2957,10 @@ ${runtimeTag}`;
3376
2957
  stamped
3377
2958
  );
3378
2959
  if (!html) return c.text("not found", 404);
3379
- html = ensureHfIds3(await transformPreviewHtml(html, adapter, project, compPath));
2960
+ html = ensureHfIds2(await transformPreviewHtml(html, adapter, project, compPath));
3380
2961
  html = injectStudioPreviewAugmentations(html, adapter, project.dir, compPath);
3381
2962
  if (previewVariables) html = injectPreviewVariables(html, previewVariables);
2963
+ html = await injectMediaCodecMap(html, adapter, project.dir, compPath, mediaCodecProbeCache);
3382
2964
  return c.html(html, 200, previewCacheHeaders(etag));
3383
2965
  });
3384
2966
  api.get("/projects/:id/preview/*", async (c) => {
@@ -3397,15 +2979,49 @@ ${runtimeTag}`;
3397
2979
  }
3398
2980
  const contentType = getMimeType(subPath);
3399
2981
  const isText = /\.(html|css|js|json|svg|txt|md|cube)$/i.test(subPath);
3400
- const etag = `"${stat.mtimeMs.toString(36)}-${stat.size.toString(36)}"`;
3401
- const cacheHeaders = isText ? { "Cache-Control": "no-store" } : { "Cache-Control": "private, max-age=3600, must-revalidate", ETag: etag };
2982
+ const proxyParam = c.req.query("hf-proxy");
2983
+ let proxyVariant;
2984
+ if (proxyParam !== void 0) {
2985
+ if (!isProxyVariantRequest(proxyParam) || !contentType.startsWith("video/") || !isAutoProxyEnabled(adapter)) {
2986
+ return c.text("not found", 404);
2987
+ }
2988
+ const facts = await probeAssetCodec(file);
2989
+ const eligibility = decideMediaProxyEligibility(facts);
2990
+ if (!eligibility.eligible) {
2991
+ return c.text(`media proxy unavailable: ${eligibility.reason}`, 422);
2992
+ }
2993
+ if (!facts) return c.text("media proxy unavailable: unknown_codec", 422);
2994
+ proxyVariant = resolveProxyVariantRequest(proxyParam, facts) ?? void 0;
2995
+ if (!proxyVariant) {
2996
+ return c.text("media proxy variant does not match asset", 422);
2997
+ }
2998
+ }
2999
+ const etag = `"${stat.mtimeMs.toString(36)}-${stat.size.toString(36)}${proxyEtagSalt(proxyVariant)}"`;
3000
+ const cacheHeaders = isText ? { "Cache-Control": "no-store" } : {
3001
+ "Cache-Control": "private, max-age=3600, must-revalidate",
3002
+ ETag: etag
3003
+ };
3402
3004
  if (!isText) {
3403
3005
  const ifNoneMatch = c.req.header("If-None-Match");
3404
3006
  if (ifNoneMatch === etag) {
3405
3007
  return new Response(null, { status: 304, headers: cacheHeaders });
3406
3008
  }
3407
3009
  }
3408
- const buffer = isText ? Buffer.from(readFileSync7(file, "utf-8"), "utf-8") : readFileSync7(file);
3010
+ let servedPath = file;
3011
+ let servedContentType = contentType;
3012
+ if (proxyVariant !== void 0) {
3013
+ try {
3014
+ servedPath = await resolveProxy(project.dir, file, proxyVariant);
3015
+ } catch (err) {
3016
+ if (err instanceof ProxyCapacityError) {
3017
+ return c.text(err.message, 503, { "Retry-After": "5" });
3018
+ }
3019
+ const message = err instanceof ProxyTranscodeError ? err.message : "proxy transcode failed";
3020
+ return c.text(message, 502);
3021
+ }
3022
+ servedContentType = PROXY_VARIANT_CONFIG[proxyVariant].contentType;
3023
+ }
3024
+ const buffer = isText ? Buffer.from(readFileSync7(file, "utf-8"), "utf-8") : readFileSync7(servedPath);
3409
3025
  const totalSize = buffer.length;
3410
3026
  const rangeHeader = c.req.header("Range");
3411
3027
  if (rangeHeader) {
@@ -3419,7 +3035,7 @@ ${runtimeTag}`;
3419
3035
  status: 206,
3420
3036
  headers: {
3421
3037
  ...cacheHeaders,
3422
- "Content-Type": contentType,
3038
+ "Content-Type": servedContentType,
3423
3039
  "Content-Range": `bytes ${start}-${safeEnd}/${totalSize}`,
3424
3040
  "Accept-Ranges": "bytes",
3425
3041
  "Content-Length": String(chunkSize)
@@ -3430,7 +3046,7 @@ ${runtimeTag}`;
3430
3046
  return new Response(new Uint8Array(buffer), {
3431
3047
  headers: {
3432
3048
  ...cacheHeaders,
3433
- "Content-Type": contentType,
3049
+ "Content-Type": servedContentType,
3434
3050
  "Accept-Ranges": "bytes",
3435
3051
  "Content-Length": String(totalSize)
3436
3052
  }
@@ -3706,587 +3322,7 @@ function registerRenderRoutes(api, adapter) {
3706
3322
  // src/routes/thumbnail.ts
3707
3323
  import { existsSync as existsSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, statSync as statSync4 } from "fs";
3708
3324
  import { join as join11 } from "path";
3709
- import { createHash as createHash3 } from "crypto";
3710
-
3711
- // src/helpers/manualEditsRenderScript.ts
3712
- var STUDIO_MANUAL_EDITS_PATH = ".hyperframes/studio-manual-edits.json";
3713
- function createStudioManualEditsRenderBodyScript(manifestContent, options = {}) {
3714
- if (!manifestContent.trim()) return null;
3715
- return `(${studioManualEditsRenderRuntime.toString()})(${JSON.stringify(manifestContent)}, ${JSON.stringify(options.activeCompositionPath ?? null)});`;
3716
- }
3717
- function createStudioPositionSeekReapplyScript() {
3718
- return `(${studioPositionSeekReapplyRuntime.toString()})();`;
3719
- }
3720
- function studioPositionSeekReapplyRuntime() {
3721
- const OFFSET_X_PROP = "--hf-studio-offset-x";
3722
- const OFFSET_Y_PROP = "--hf-studio-offset-y";
3723
- const WIDTH_PROP = "--hf-studio-width";
3724
- const HEIGHT_PROP = "--hf-studio-height";
3725
- const ROTATION_PROP = "--hf-studio-rotation";
3726
- const PATH_OFFSET_ATTR = "data-hf-studio-path-offset";
3727
- const BOX_SIZE_ATTR = "data-hf-studio-box-size";
3728
- const ROTATION_ATTR = "data-hf-studio-rotation";
3729
- const ORIGINAL_TRANSLATE_ATTR = "data-hf-studio-original-translate";
3730
- const ORIGINAL_ROTATE_ATTR = "data-hf-studio-original-rotate";
3731
- const MOTION_ATTR = "data-hf-studio-motion";
3732
- const MOTION_TL_KEY = "studio-motion";
3733
- const WRAPPED_PROP = "__hfStudioPositionSeekReapplyWrapped";
3734
- if (!document.querySelector("[" + PATH_OFFSET_ATTR + '="true"]') && !document.querySelector("[" + BOX_SIZE_ATTR + '="true"]') && !document.querySelector("[" + ROTATION_ATTR + '="true"]') && !document.querySelector("[" + MOTION_ATTR + "]"))
3735
- return;
3736
- const splitTopLevelWhitespace = (value) => {
3737
- const parts = [];
3738
- let depth = 0;
3739
- let current = "";
3740
- for (const char of value.trim()) {
3741
- if (char === "(") depth += 1;
3742
- if (char === ")") depth = Math.max(0, depth - 1);
3743
- if (/\s/.test(char) && depth === 0) {
3744
- if (current) parts.push(current);
3745
- current = "";
3746
- } else {
3747
- current += char;
3748
- }
3749
- }
3750
- if (current) parts.push(current);
3751
- return parts;
3752
- };
3753
- const composeTranslate = (element, x, y) => {
3754
- const original = element.getAttribute(ORIGINAL_TRANSLATE_ATTR)?.trim();
3755
- if (!original || original === "none") return x + " " + y;
3756
- const parts = splitTopLevelWhitespace(original);
3757
- if (parts.length === 1) return "calc(" + parts[0] + " + " + x + ") " + y;
3758
- if (parts.length >= 2) {
3759
- const z = parts.length >= 3 ? " " + parts[2] : "";
3760
- return "calc(" + parts[0] + " + " + x + ") calc(" + parts[1] + " + " + y + ")" + z;
3761
- }
3762
- return x + " " + y;
3763
- };
3764
- const isSimpleRotateAngle = (value) => /^-?(?:\d+(?:\.\d+)?|\.\d+)(?:deg|rad|turn|grad)$/.test(value.trim());
3765
- const composeRotation = (element, rotationValue) => {
3766
- const original = element.getAttribute(ORIGINAL_ROTATE_ATTR)?.trim();
3767
- if (!original || original === "none" || !isSimpleRotateAngle(original)) return rotationValue;
3768
- return "calc(" + original + " + " + rotationValue + ")";
3769
- };
3770
- let lastSeekTime = 0;
3771
- let cachedMotionKey = "";
3772
- const finiteNum = (v) => typeof v === "number" && Number.isFinite(v) ? v : null;
3773
- const computeMotionKey = (motionEls) => {
3774
- let key = "";
3775
- for (let i = 0; i < motionEls.length; i++) {
3776
- const json = motionEls[i].getAttribute?.(MOTION_ATTR);
3777
- if (json) key += (key ? "\n" : "") + json;
3778
- }
3779
- return key;
3780
- };
3781
- const reapplyMotionTimeline = () => {
3782
- const motionEls = document.querySelectorAll("[" + MOTION_ATTR + "]");
3783
- if (motionEls.length === 0) {
3784
- cachedMotionKey = "";
3785
- return;
3786
- }
3787
- const win = window;
3788
- const gsap = win.gsap;
3789
- if (!gsap || typeof gsap.timeline !== "function") return;
3790
- win.__timelines = win.__timelines || {};
3791
- const motionKey = computeMotionKey(motionEls);
3792
- const existing = win.__timelines[MOTION_TL_KEY];
3793
- if (motionKey && motionKey === cachedMotionKey && existing && typeof existing.totalTime === "function") {
3794
- existing.totalTime(lastSeekTime, false);
3795
- return;
3796
- }
3797
- if (existing && typeof existing.kill === "function") existing.kill();
3798
- const tl = gsap.timeline({ paused: true, defaults: { overwrite: "auto" } });
3799
- const fromTo = tl.fromTo;
3800
- if (typeof fromTo !== "function") return;
3801
- let applied = 0;
3802
- for (let i = 0; i < motionEls.length; i++) {
3803
- const el = motionEls[i];
3804
- if (!(el instanceof HTMLElement)) continue;
3805
- const json = el.getAttribute(MOTION_ATTR);
3806
- if (!json) continue;
3807
- try {
3808
- const m = JSON.parse(json);
3809
- const start = finiteNum(m.start);
3810
- const duration = finiteNum(m.duration);
3811
- if (start == null || duration == null || duration <= 0) continue;
3812
- const ease = typeof m.ease === "string" ? m.ease : "none";
3813
- const from = m.from && typeof m.from === "object" ? m.from : {};
3814
- const to = m.to && typeof m.to === "object" ? m.to : {};
3815
- const customEase = m.customEase;
3816
- let resolvedEase = ease;
3817
- if (customEase?.id && customEase?.data && win.CustomEase?.create) {
3818
- try {
3819
- gsap.registerPlugin?.(win.CustomEase);
3820
- win.CustomEase.create(customEase.id, customEase.data);
3821
- resolvedEase = customEase.id;
3822
- } catch {
3823
- }
3824
- }
3825
- fromTo.call(
3826
- tl,
3827
- el,
3828
- { ...from },
3829
- { ...to, duration, ease: resolvedEase, overwrite: "auto", immediateRender: false },
3830
- start
3831
- );
3832
- applied += 1;
3833
- } catch {
3834
- }
3835
- }
3836
- if (applied === 0) {
3837
- cachedMotionKey = "";
3838
- if (typeof tl.kill === "function")
3839
- tl.kill();
3840
- return;
3841
- }
3842
- cachedMotionKey = motionKey;
3843
- win.__timelines[MOTION_TL_KEY] = tl;
3844
- if (typeof tl.pause === "function") tl.pause();
3845
- if (typeof tl.totalTime === "function")
3846
- tl.totalTime(lastSeekTime, false);
3847
- };
3848
- const stripGsapTranslateFromTransform = (el) => {
3849
- const transform = el.style.getPropertyValue("transform");
3850
- if (!transform || transform === "none") return;
3851
- const win = el.ownerDocument.defaultView;
3852
- const MatrixCtor = win?.DOMMatrix;
3853
- if (!MatrixCtor) return;
3854
- try {
3855
- const m = new MatrixCtor(transform);
3856
- if (m.m41 === 0 && m.m42 === 0) return;
3857
- m.m41 = 0;
3858
- m.m42 = 0;
3859
- if (m.is2D && m.a === 1 && m.b === 0 && m.c === 0 && m.d === 1) {
3860
- el.style.removeProperty("transform");
3861
- } else {
3862
- el.style.setProperty("transform", m.toString());
3863
- }
3864
- } catch {
3865
- }
3866
- };
3867
- const reapplyAll = () => {
3868
- const offsetEls = document.querySelectorAll("[" + PATH_OFFSET_ATTR + '="true"]');
3869
- for (let i = 0; i < offsetEls.length; i++) {
3870
- const el = offsetEls[i];
3871
- if (!(el instanceof HTMLElement)) continue;
3872
- const x = el.style.getPropertyValue(OFFSET_X_PROP);
3873
- const y = el.style.getPropertyValue(OFFSET_Y_PROP);
3874
- if (x || y) {
3875
- el.style.setProperty(
3876
- "translate",
3877
- composeTranslate(
3878
- el,
3879
- "var(" + OFFSET_X_PROP + ", 0px)",
3880
- "var(" + OFFSET_Y_PROP + ", 0px)"
3881
- )
3882
- );
3883
- stripGsapTranslateFromTransform(el);
3884
- }
3885
- }
3886
- const boxSizeEls = document.querySelectorAll("[" + BOX_SIZE_ATTR + '="true"]');
3887
- for (let i = 0; i < boxSizeEls.length; i++) {
3888
- const el = boxSizeEls[i];
3889
- if (!(el instanceof HTMLElement)) continue;
3890
- const w = el.style.getPropertyValue(WIDTH_PROP);
3891
- const h = el.style.getPropertyValue(HEIGHT_PROP);
3892
- if (w) el.style.setProperty("width", w);
3893
- if (h) el.style.setProperty("height", h);
3894
- }
3895
- const rotEls = document.querySelectorAll("[" + ROTATION_ATTR + '="true"]');
3896
- for (let i = 0; i < rotEls.length; i++) {
3897
- const el = rotEls[i];
3898
- if (!(el instanceof HTMLElement)) continue;
3899
- const rot = el.style.getPropertyValue(ROTATION_PROP);
3900
- if (rot) {
3901
- el.style.setProperty("rotate", composeRotation(el, "var(" + ROTATION_PROP + ", 0deg)"));
3902
- stripGsapTranslateFromTransform(el);
3903
- }
3904
- }
3905
- reapplyMotionTimeline();
3906
- };
3907
- const runtimeWindow = window;
3908
- const isWrapped = (fn) => Boolean(fn[WRAPPED_PROP]);
3909
- const markWrapped = (fn) => {
3910
- try {
3911
- Object.defineProperty(fn, WRAPPED_PROP, {
3912
- configurable: false,
3913
- enumerable: false,
3914
- value: true
3915
- });
3916
- } catch {
3917
- try {
3918
- fn[WRAPPED_PROP] = true;
3919
- } catch {
3920
- }
3921
- }
3922
- };
3923
- const wrapFn = (get, set) => {
3924
- const fn = get();
3925
- if (typeof fn !== "function") return false;
3926
- const seek = fn;
3927
- if (isWrapped(seek)) {
3928
- reapplyAll();
3929
- return true;
3930
- }
3931
- const wrapped = function(time) {
3932
- lastSeekTime = typeof time === "number" && Number.isFinite(time) ? Math.max(0, time) : 0;
3933
- const result = seek.call(this, time);
3934
- reapplyAll();
3935
- return result;
3936
- };
3937
- markWrapped(wrapped);
3938
- set(wrapped);
3939
- reapplyAll();
3940
- return true;
3941
- };
3942
- const wrapSeekFunctions = () => {
3943
- const a = wrapFn(
3944
- () => runtimeWindow.__hf?.["seek"],
3945
- (fn) => {
3946
- if (runtimeWindow.__hf) runtimeWindow.__hf["seek"] = fn;
3947
- }
3948
- );
3949
- const b = wrapFn(
3950
- () => runtimeWindow.__player?.["renderSeek"],
3951
- (fn) => {
3952
- if (runtimeWindow.__player) runtimeWindow.__player["renderSeek"] = fn;
3953
- }
3954
- );
3955
- return a || b;
3956
- };
3957
- const installSeekTrap = (obj, key, getter, setter) => {
3958
- if (!obj) return;
3959
- try {
3960
- let current = obj[key];
3961
- Object.defineProperty(obj, key, {
3962
- configurable: true,
3963
- enumerable: true,
3964
- get() {
3965
- return current;
3966
- },
3967
- set(value) {
3968
- current = value;
3969
- if (typeof value === "function" && !isWrapped(value)) {
3970
- wrapFn(getter, setter);
3971
- }
3972
- }
3973
- });
3974
- } catch {
3975
- }
3976
- };
3977
- if (document.readyState === "loading") {
3978
- document.addEventListener("DOMContentLoaded", () => reapplyAll(), { once: true });
3979
- } else {
3980
- reapplyAll();
3981
- }
3982
- wrapSeekFunctions();
3983
- installSeekTrap(
3984
- runtimeWindow.__hf,
3985
- "seek",
3986
- () => runtimeWindow.__hf?.["seek"],
3987
- (fn) => {
3988
- if (runtimeWindow.__hf) runtimeWindow.__hf["seek"] = fn;
3989
- }
3990
- );
3991
- installSeekTrap(
3992
- runtimeWindow.__player,
3993
- "renderSeek",
3994
- () => runtimeWindow.__player?.["renderSeek"],
3995
- (fn) => {
3996
- if (runtimeWindow.__player) runtimeWindow.__player["renderSeek"] = fn;
3997
- }
3998
- );
3999
- let remaining = 120;
4000
- const interval = setInterval(() => {
4001
- wrapSeekFunctions();
4002
- remaining -= 1;
4003
- if (remaining <= 0) clearInterval(interval);
4004
- }, 50);
4005
- }
4006
- function studioManualEditsRenderRuntime(manifestContent, activeCompositionPath) {
4007
- const OFFSET_X_PROP = "--hf-studio-offset-x";
4008
- const OFFSET_Y_PROP = "--hf-studio-offset-y";
4009
- const WIDTH_PROP = "--hf-studio-width";
4010
- const HEIGHT_PROP = "--hf-studio-height";
4011
- const ROTATION_PROP = "--hf-studio-rotation";
4012
- const PATH_OFFSET_ATTR = "data-hf-studio-path-offset";
4013
- const BOX_SIZE_ATTR = "data-hf-studio-box-size";
4014
- const ROTATION_ATTR = "data-hf-studio-rotation";
4015
- const ORIGINAL_TRANSLATE_ATTR = "data-hf-studio-original-translate";
4016
- const ORIGINAL_ROTATE_ATTR = "data-hf-studio-original-rotate";
4017
- const WRAPPED_SEEK_PROP = "__hfStudioManualEditsWrapped";
4018
- const ROTATION_TRANSFORM_ORIGIN = "center center";
4019
- const finiteNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
4020
- const objectRecord = (value) => value && typeof value === "object" ? value : null;
4021
- const runtimeWindow = window;
4022
- const parsedManifest = (() => {
4023
- try {
4024
- return objectRecord(JSON.parse(manifestContent));
4025
- } catch {
4026
- return null;
4027
- }
4028
- })();
4029
- const manifestEdits = Array.isArray(parsedManifest?.edits) ? parsedManifest.edits : [];
4030
- if (manifestEdits.length === 0) return;
4031
- const sourceFileForElement = (element) => {
4032
- let current = element;
4033
- while (current) {
4034
- const sourceFile = current.getAttribute("data-composition-file") ?? current.getAttribute("data-composition-src");
4035
- if (sourceFile) return sourceFile;
4036
- current = current.parentElement;
4037
- }
4038
- return activeCompositionPath ?? "index.html";
4039
- };
4040
- const elementMatchesSourceFile = (element, sourceFile) => sourceFileForElement(element) === sourceFile;
4041
- const styleUsesStudioOffset = (value) => value.includes(OFFSET_X_PROP) || value.includes(OFFSET_Y_PROP);
4042
- const styleUsesStudioRotation = (value) => value.includes(ROTATION_PROP);
4043
- const splitTopLevelWhitespace = (value) => {
4044
- const parts = [];
4045
- let depth = 0;
4046
- let current = "";
4047
- for (const char of value.trim()) {
4048
- if (char === "(") depth += 1;
4049
- if (char === ")") depth = Math.max(0, depth - 1);
4050
- if (/\s/.test(char) && depth === 0) {
4051
- if (current) parts.push(current);
4052
- current = "";
4053
- } else {
4054
- current += char;
4055
- }
4056
- }
4057
- if (current) parts.push(current);
4058
- return parts;
4059
- };
4060
- const composeTranslate = (element, x, y) => {
4061
- const original = element.getAttribute(ORIGINAL_TRANSLATE_ATTR)?.trim();
4062
- if (!original || original === "none") return `${x} ${y}`;
4063
- const parts = splitTopLevelWhitespace(original);
4064
- if (parts.length === 1) return `calc(${parts[0]} + ${x}) ${y}`;
4065
- if (parts.length === 2) return `calc(${parts[0]} + ${x}) calc(${parts[1]} + ${y})`;
4066
- if (parts.length === 3) {
4067
- return `calc(${parts[0]} + ${x}) calc(${parts[1]} + ${y}) ${parts[2]}`;
4068
- }
4069
- return `${x} ${y}`;
4070
- };
4071
- const readStyleOrComputed = (element, property) => {
4072
- try {
4073
- return element.style.getPropertyValue(property) || getComputedStyle(element).getPropertyValue(property);
4074
- } catch {
4075
- return element.style.getPropertyValue(property);
4076
- }
4077
- };
4078
- const readTransformLonghandBase = (element, property) => {
4079
- const value = readStyleOrComputed(element, property).trim();
4080
- return value === "none" ? "" : value;
4081
- };
4082
- const preparePathOffsetBase = (element) => {
4083
- const currentTranslate = readTransformLonghandBase(element, "translate");
4084
- const hasMarker = element.hasAttribute(PATH_OFFSET_ATTR);
4085
- const wasResetByAnimation = !styleUsesStudioOffset(currentTranslate);
4086
- if (!hasMarker) {
4087
- element.setAttribute(ORIGINAL_TRANSLATE_ATTR, wasResetByAnimation ? currentTranslate : "");
4088
- } else if (wasResetByAnimation) {
4089
- element.setAttribute(ORIGINAL_TRANSLATE_ATTR, currentTranslate);
4090
- }
4091
- };
4092
- const prepareRotationBase = (element) => {
4093
- const currentRotate = readTransformLonghandBase(element, "rotate");
4094
- const hasMarker = element.hasAttribute(ROTATION_ATTR);
4095
- const wasResetByAnimation = !styleUsesStudioRotation(currentRotate);
4096
- if (!hasMarker) {
4097
- element.setAttribute(ORIGINAL_ROTATE_ATTR, wasResetByAnimation ? currentRotate : "");
4098
- } else if (wasResetByAnimation) {
4099
- element.setAttribute(ORIGINAL_ROTATE_ATTR, currentRotate);
4100
- }
4101
- };
4102
- const querySelectorCandidates = (selector) => {
4103
- const isCandidate = (element) => element instanceof HTMLElement;
4104
- const className = selector.match(/^\.([A-Za-z0-9_-]+)$/)?.[1];
4105
- if (className) {
4106
- return Array.from(document.getElementsByTagName("*")).filter(
4107
- (element) => isCandidate(element) && element.classList.contains(className)
4108
- );
4109
- }
4110
- if (/^[A-Za-z][A-Za-z0-9-]*$/.test(selector)) {
4111
- return Array.from(document.getElementsByTagName(selector)).filter(isCandidate);
4112
- }
4113
- return Array.from(document.querySelectorAll(selector)).filter(isCandidate);
4114
- };
4115
- const resolveTarget = (edit) => {
4116
- const targetRecord = objectRecord(edit.target);
4117
- if (!targetRecord) return null;
4118
- const sourceFile = typeof targetRecord.sourceFile === "string" ? targetRecord.sourceFile : "";
4119
- if (!sourceFile) return null;
4120
- const id = typeof targetRecord.id === "string" ? targetRecord.id : "";
4121
- if (id) {
4122
- const byId = document.getElementById(id);
4123
- if (byId instanceof HTMLElement && elementMatchesSourceFile(byId, sourceFile)) return byId;
4124
- const matchesById = [
4125
- document.documentElement,
4126
- ...Array.from(document.getElementsByTagName("*"))
4127
- ].filter(
4128
- (element) => element instanceof HTMLElement && element.id === id && elementMatchesSourceFile(element, sourceFile)
4129
- );
4130
- if (matchesById[0]) return matchesById[0];
4131
- }
4132
- const selector = typeof targetRecord.selector === "string" ? targetRecord.selector : "";
4133
- if (!selector) return null;
4134
- try {
4135
- const matches = querySelectorCandidates(selector).filter(
4136
- (element) => elementMatchesSourceFile(element, sourceFile)
4137
- );
4138
- const selectorIndex = finiteNumber(targetRecord.selectorIndex) ?? 0;
4139
- return matches[Math.max(0, Math.floor(selectorIndex))] ?? null;
4140
- } catch {
4141
- return null;
4142
- }
4143
- };
4144
- const roundRotationAngle = (angle) => Math.round(angle * 10) / 10;
4145
- const isSimpleRotateAngle = (value) => /^-?(?:\d+(?:\.\d+)?|\.\d+)(?:deg|rad|turn|grad)$/.test(value.trim());
4146
- const composeRotation = (element, rotationValue) => {
4147
- const original = element.getAttribute(ORIGINAL_ROTATE_ATTR)?.trim();
4148
- if (!original || original === "none" || !isSimpleRotateAngle(original)) {
4149
- return rotationValue;
4150
- }
4151
- return `calc(${original} + ${rotationValue})`;
4152
- };
4153
- const applyPathOffset = (element, edit) => {
4154
- const x = finiteNumber(edit.x);
4155
- const y = finiteNumber(edit.y);
4156
- if (x == null || y == null) return;
4157
- preparePathOffsetBase(element);
4158
- element.setAttribute(PATH_OFFSET_ATTR, "true");
4159
- element.style.setProperty(OFFSET_X_PROP, `${Math.round(x)}px`);
4160
- element.style.setProperty(OFFSET_Y_PROP, `${Math.round(y)}px`);
4161
- element.style.setProperty(
4162
- "translate",
4163
- composeTranslate(element, `var(${OFFSET_X_PROP}, 0px)`, `var(${OFFSET_Y_PROP}, 0px)`)
4164
- );
4165
- };
4166
- const readParentFlexBasisPixels = (element, size) => {
4167
- const parent = element.parentElement;
4168
- if (!parent) return null;
4169
- const styles = getComputedStyle(parent);
4170
- if (styles.display !== "flex" && styles.display !== "inline-flex") return null;
4171
- return Math.round(
4172
- Math.max(1, styles.flexDirection.startsWith("column") ? size.height : size.width)
4173
- );
4174
- };
4175
- const applyBoxSize = (element, edit) => {
4176
- const width = finiteNumber(edit.width);
4177
- const height = finiteNumber(edit.height);
4178
- if (width == null || height == null || width <= 0 || height <= 0) return;
4179
- const rounded = {
4180
- width: Math.round(Math.max(1, width)),
4181
- height: Math.round(Math.max(1, height))
4182
- };
4183
- element.setAttribute(BOX_SIZE_ATTR, "true");
4184
- element.style.setProperty(WIDTH_PROP, `${rounded.width}px`);
4185
- element.style.setProperty(HEIGHT_PROP, `${rounded.height}px`);
4186
- element.style.setProperty("box-sizing", "border-box");
4187
- element.style.setProperty("width", `${rounded.width}px`);
4188
- element.style.setProperty("height", `${rounded.height}px`);
4189
- element.style.setProperty("min-width", "0px");
4190
- element.style.setProperty("min-height", "0px");
4191
- element.style.setProperty("max-width", "none");
4192
- element.style.setProperty("max-height", "none");
4193
- const flexBasis = readParentFlexBasisPixels(element, rounded);
4194
- if (flexBasis != null) {
4195
- element.style.setProperty("flex-basis", `${flexBasis}px`);
4196
- element.style.setProperty("flex-grow", "0");
4197
- element.style.setProperty("flex-shrink", "0");
4198
- }
4199
- if (getComputedStyle(element).display === "inline") {
4200
- element.style.setProperty("display", "inline-block");
4201
- }
4202
- };
4203
- const applyRotation = (element, edit) => {
4204
- const angle = finiteNumber(edit.angle);
4205
- if (angle == null) return;
4206
- prepareRotationBase(element);
4207
- element.setAttribute(ROTATION_ATTR, "true");
4208
- element.style.setProperty(ROTATION_PROP, `${roundRotationAngle(angle)}deg`);
4209
- element.style.setProperty("transform-origin", ROTATION_TRANSFORM_ORIGIN);
4210
- element.style.setProperty("rotate", composeRotation(element, `var(${ROTATION_PROP}, 0deg)`));
4211
- };
4212
- const applyManifest = () => {
4213
- let applied = 0;
4214
- for (const edit of manifestEdits) {
4215
- const editRecord = objectRecord(edit);
4216
- if (!editRecord) continue;
4217
- const element = resolveTarget(editRecord);
4218
- if (!element) continue;
4219
- if (editRecord.kind === "path-offset") applyPathOffset(element, editRecord);
4220
- if (editRecord.kind === "box-size") applyBoxSize(element, editRecord);
4221
- if (editRecord.kind === "rotation") applyRotation(element, editRecord);
4222
- applied += 1;
4223
- }
4224
- return applied;
4225
- };
4226
- runtimeWindow.__hfStudioManualEditsApply = applyManifest;
4227
- const markWrapped = (fn) => {
4228
- try {
4229
- Object.defineProperty(fn, WRAPPED_SEEK_PROP, {
4230
- configurable: false,
4231
- enumerable: false,
4232
- value: true
4233
- });
4234
- } catch {
4235
- try {
4236
- fn[WRAPPED_SEEK_PROP] = true;
4237
- } catch {
4238
- }
4239
- }
4240
- };
4241
- const isWrapped = (fn) => Boolean(fn[WRAPPED_SEEK_PROP]);
4242
- const wrapFunction = (get, set) => {
4243
- const fn = get();
4244
- if (!fn) return false;
4245
- const seek = fn;
4246
- if (isWrapped(seek)) {
4247
- applyManifest();
4248
- return true;
4249
- }
4250
- const wrappedSeek = function(time) {
4251
- const result = seek.call(this, time);
4252
- applyManifest();
4253
- return result;
4254
- };
4255
- markWrapped(wrappedSeek);
4256
- set(wrappedSeek);
4257
- applyManifest();
4258
- return true;
4259
- };
4260
- const wrapSeekFunctions = () => {
4261
- const wrappedHfSeek = wrapFunction(
4262
- () => runtimeWindow.__hf?.seek,
4263
- (fn) => {
4264
- if (runtimeWindow.__hf) runtimeWindow.__hf.seek = fn;
4265
- }
4266
- );
4267
- const wrappedPlayerRenderSeek = wrapFunction(
4268
- () => runtimeWindow.__player?.renderSeek,
4269
- (fn) => {
4270
- if (runtimeWindow.__player) runtimeWindow.__player.renderSeek = fn;
4271
- }
4272
- );
4273
- return wrappedHfSeek || wrappedPlayerRenderSeek;
4274
- };
4275
- if (document.readyState === "loading") {
4276
- document.addEventListener("DOMContentLoaded", () => applyManifest(), { once: true });
4277
- } else {
4278
- applyManifest();
4279
- }
4280
- wrapSeekFunctions();
4281
- let remainingSeekWrapAttempts = 120;
4282
- const seekWrapInterval = setInterval(() => {
4283
- wrapSeekFunctions();
4284
- remainingSeekWrapAttempts -= 1;
4285
- if (remainingSeekWrapAttempts <= 0) clearInterval(seekWrapInterval);
4286
- }, 50);
4287
- }
4288
-
4289
- // src/routes/thumbnail.ts
3325
+ import { createHash as createHash4 } from "crypto";
4290
3326
  var THUMBNAIL_CACHE_VERSION = "v4";
4291
3327
  function registerThumbnailRoutes(api, adapter) {
4292
3328
  api.get("/projects/:id/thumbnail/*", async (c) => {
@@ -4318,7 +3354,7 @@ function registerThumbnailRoutes(api, adapter) {
4318
3354
  const htmlFile = join11(project.dir, compPath);
4319
3355
  if (existsSync7(htmlFile)) {
4320
3356
  const html = readFileSync10(htmlFile, "utf-8");
4321
- sourceKey = `_${createHash3("sha1").update(html).digest("hex").slice(0, 16)}`;
3357
+ sourceKey = `_${createHash4("sha1").update(html).digest("hex").slice(0, 16)}`;
4322
3358
  sourceMtime = Math.round(statSync4(htmlFile).mtimeMs);
4323
3359
  if (!vpWidth) {
4324
3360
  const wMatch = html.match(/data-width=["'](\d+)["']/);
@@ -4331,14 +3367,14 @@ function registerThumbnailRoutes(api, adapter) {
4331
3367
  let manualEditsKey = "";
4332
3368
  if (existsSync7(manualEditsFile)) {
4333
3369
  const manualEditsContent = readFileSync10(manualEditsFile, "utf-8");
4334
- manualEditsKey = `_${createHash3("sha1").update(manualEditsContent).digest("hex").slice(0, 16)}`;
3370
+ manualEditsKey = `_${createHash4("sha1").update(manualEditsContent).digest("hex").slice(0, 16)}`;
4335
3371
  sourceMtime = Math.max(sourceMtime, Math.round(statSync4(manualEditsFile).mtimeMs));
4336
3372
  }
4337
3373
  const motionFile = join11(project.dir, STUDIO_MOTION_PATH);
4338
3374
  let motionKey = "";
4339
3375
  if (existsSync7(motionFile)) {
4340
3376
  const motionContent = readFileSync10(motionFile, "utf-8");
4341
- motionKey = `_${createHash3("sha1").update(motionContent).digest("hex").slice(0, 16)}`;
3377
+ motionKey = `_${createHash4("sha1").update(motionContent).digest("hex").slice(0, 16)}`;
4342
3378
  sourceMtime = Math.max(sourceMtime, Math.round(statSync4(motionFile).mtimeMs));
4343
3379
  }
4344
3380
  const previewUrl = compPath === "index.html" ? `http://${c.req.header("host")}/api/projects/${project.id}/preview` : `http://${c.req.header("host")}/api/projects/${project.id}/preview/comp/${compPath}`;
@@ -4419,7 +3455,7 @@ function registerWaveformRoutes(api, adapter) {
4419
3455
  }
4420
3456
 
4421
3457
  // src/routes/fonts.ts
4422
- import { closeSync as closeSync2, constants as constants2, fstatSync as fstatSync2, openSync as openSync2, readSync } from "fs";
3458
+ import { closeSync as closeSync3, constants as constants2, fstatSync as fstatSync2, openSync as openSync3, readSync } from "fs";
4423
3459
  import {
4424
3460
  collectFontFileEntries,
4425
3461
  fontDirectories,
@@ -4526,7 +3562,7 @@ function registerFontRoutes(api) {
4526
3562
  if (!located) return c.json({ error: "font not found" }, 404);
4527
3563
  let fd;
4528
3564
  try {
4529
- fd = openSync2(located.path, constants2.O_RDONLY | constants2.O_NOFOLLOW);
3565
+ fd = openSync3(located.path, constants2.O_RDONLY | constants2.O_NOFOLLOW);
4530
3566
  } catch {
4531
3567
  return c.json({ error: "font file not accessible" }, 404);
4532
3568
  }
@@ -4548,7 +3584,7 @@ function registerFontRoutes(api) {
4548
3584
  } catch {
4549
3585
  return c.json({ error: "failed to read font file" }, 500);
4550
3586
  } finally {
4551
- closeSync2(fd);
3587
+ closeSync3(fd);
4552
3588
  }
4553
3589
  });
4554
3590
  }
@@ -4683,101 +3719,28 @@ function registerSelectionRoutes(api, adapter) {
4683
3719
  // src/routes/media.ts
4684
3720
  import { streamSSE as streamSSE2 } from "hono/streaming";
4685
3721
  import { existsSync as existsSync9, mkdirSync as mkdirSync7 } from "fs";
4686
- import { basename as basename2, dirname as dirname2, extname as extname3, join as join13 } from "path";
4687
-
4688
- // src/helpers/mediaMetadata.ts
4689
- import { spawnSync as spawnSync2 } from "child_process";
4690
- import { extname as extname2 } from "path";
4691
- var VIDEO_EXT2 = /* @__PURE__ */ new Set([".mp4", ".mov", ".webm", ".mkv", ".avi", ".m4v"]);
4692
- var IMAGE_EXT = /* @__PURE__ */ new Set([".jpg", ".jpeg", ".png", ".webp", ".avif"]);
4693
- var AUDIO_EXT2 = /* @__PURE__ */ new Set([".mp3", ".wav", ".ogg", ".m4a", ".aac"]);
4694
- function lower(value) {
4695
- return value?.toLowerCase() ?? "";
4696
- }
4697
- function inferKindFromPath(path) {
4698
- const ext = extname2(path).toLowerCase();
4699
- if (VIDEO_EXT2.has(ext)) return "video";
4700
- if (IMAGE_EXT.has(ext)) return "image";
4701
- if (AUDIO_EXT2.has(ext)) return "audio";
4702
- return "unknown";
4703
- }
4704
- function colorLabel(input) {
4705
- if (input.isHdr) {
4706
- if (input.hdrTransfer === "pq") return "HDR PQ";
4707
- if (input.hdrTransfer === "hlg") return "HDR HLG";
4708
- return "HDR";
4709
- }
4710
- if (input.colorPrimaries.includes("bt709") || input.colorSpace.includes("bt709") || input.colorTransfer.includes("bt709")) {
4711
- return "SDR Rec.709";
4712
- }
4713
- return "SDR/unknown";
4714
- }
4715
- function classifyMediaColor(stream) {
4716
- const colorPrimaries = lower(stream?.color_primaries);
4717
- const colorSpace = lower(stream?.color_space);
4718
- const colorTransfer = lower(stream?.color_transfer);
4719
- const isHdr = colorPrimaries.includes("bt2020") || colorSpace.includes("bt2020") || colorTransfer === "smpte2084" || colorTransfer === "arib-std-b67";
4720
- const hdrTransfer = isHdr ? colorTransfer === "smpte2084" ? "pq" : colorTransfer === "arib-std-b67" ? "hlg" : "unknown" : null;
4721
- return {
4722
- dynamicRange: stream ? isHdr ? "hdr" : "sdr" : "unknown",
4723
- hdrTransfer,
4724
- label: stream ? colorLabel({ isHdr, hdrTransfer, colorPrimaries, colorSpace, colorTransfer }) : "Unknown",
4725
- isHdr,
4726
- codecName: stream?.codec_name,
4727
- profile: stream?.profile,
4728
- pixelFormat: stream?.pix_fmt,
4729
- colorSpace: stream?.color_space,
4730
- colorTransfer: stream?.color_transfer,
4731
- colorPrimaries: stream?.color_primaries,
4732
- bitsPerRawSample: stream?.bits_per_raw_sample
4733
- };
4734
- }
4735
- function probeMediaMetadata(filePath, runner = spawnSync2) {
4736
- const kind = inferKindFromPath(filePath);
4737
- if (kind === "audio" || kind === "unknown") {
4738
- return { kind, color: classifyMediaColor(null) };
4739
- }
4740
- const result = runner(
4741
- "ffprobe",
4742
- [
4743
- "-v",
4744
- "error",
4745
- "-show_entries",
4746
- "stream=codec_type,codec_name,profile,pix_fmt,color_space,color_transfer,color_primaries,bits_per_raw_sample",
4747
- "-of",
4748
- "json",
4749
- filePath
4750
- ],
4751
- { timeout: 15e3, maxBuffer: 1024 * 1024 }
4752
- );
4753
- if (result.error?.code === "ENOENT") {
4754
- return { kind, color: classifyMediaColor(null), probeError: "ffprobe unavailable" };
4755
- }
4756
- if (result.status !== 0) {
4757
- return { kind, color: classifyMediaColor(null), probeError: "ffprobe failed" };
4758
- }
4759
- try {
4760
- const parsed = JSON.parse(String(result.stdout || "{}"));
4761
- const stream = parsed.streams?.find(
4762
- (item) => kind === "image" ? item.codec_type === "video" : item.codec_type === kind
4763
- );
4764
- return { kind, color: classifyMediaColor(stream) };
4765
- } catch {
4766
- return { kind, color: classifyMediaColor(null), probeError: "ffprobe returned invalid json" };
4767
- }
4768
- }
4769
-
4770
- // src/routes/media.ts
4771
- var VIDEO_EXTENSIONS = /* @__PURE__ */ new Set([".mp4", ".mov", ".webm", ".mkv", ".avi"]);
3722
+ import { basename as basename2, dirname as dirname2, extname as extname2, join as join13 } from "path";
3723
+ var VIDEO_EXTENSIONS = /* @__PURE__ */ new Set([
3724
+ ".mp4",
3725
+ ".mov",
3726
+ ".webm",
3727
+ ".mkv",
3728
+ ".avi",
3729
+ ".m4v",
3730
+ ".mxf",
3731
+ ".mts",
3732
+ ".m2ts",
3733
+ ".ts"
3734
+ ]);
4772
3735
  var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".jpg", ".jpeg", ".png", ".webp"]);
4773
3736
  var VIDEO_OUTPUT_EXTENSIONS = /* @__PURE__ */ new Set([".webm", ".mov"]);
4774
3737
  var QUALITIES = /* @__PURE__ */ new Set(["fast", "balanced", "best"]);
4775
3738
  var DEVICES = /* @__PURE__ */ new Set(["auto", "cpu", "coreml", "cuda"]);
4776
3739
  function isVideoPath(path) {
4777
- return VIDEO_EXTENSIONS.has(extname3(path).toLowerCase());
3740
+ return VIDEO_EXTENSIONS.has(extname2(path).toLowerCase());
4778
3741
  }
4779
3742
  function isImagePath(path) {
4780
- return IMAGE_EXTENSIONS.has(extname3(path).toLowerCase());
3743
+ return IMAGE_EXTENSIONS.has(extname2(path).toLowerCase());
4781
3744
  }
4782
3745
  function normalizeProjectAssetPath(path) {
4783
3746
  return path.trim().replace(/^[.]\//, "").replace(/[?#].*$/, "");
@@ -4786,11 +3749,11 @@ function containsNullByte(path) {
4786
3749
  return path.includes("\0");
4787
3750
  }
4788
3751
  function slugFileBase(path) {
4789
- const name = basename2(path, extname3(path)).replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
3752
+ const name = basename2(path, extname2(path)).replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
4790
3753
  return name || "media";
4791
3754
  }
4792
3755
  function uniqueAssetPath(projectDir, assetPath) {
4793
- const ext = extname3(assetPath);
3756
+ const ext = extname2(assetPath);
4794
3757
  const withoutExt = assetPath.slice(0, -ext.length);
4795
3758
  let candidate = assetPath;
4796
3759
  for (let index = 2; existsSync9(join13(projectDir, candidate)); index++) {
@@ -4845,7 +3808,7 @@ function registerMediaRoutes(api, adapter, options = {}) {
4845
3808
  const filePath = resolveWithinProject(project.dir, assetPath);
4846
3809
  if (!filePath) return c.json({ error: "forbidden" }, 403);
4847
3810
  if (!existsSync9(filePath)) return c.json({ error: "media not found" }, 404);
4848
- return c.json({ path: assetPath, metadata: readMediaMetadata(filePath) });
3811
+ return c.json({ path: assetPath, metadata: await readMediaMetadata(filePath) });
4849
3812
  });
4850
3813
  api.post(
4851
3814
  "/projects/:id/media/remove-background",
@@ -4882,10 +3845,10 @@ function registerMediaRoutes(api, adapter, options = {}) {
4882
3845
  const outputAssetPath = requestedOutput ? uniqueAssetPath(project.dir, requestedOutput) : defaultOutputPath(project.dir, inputAssetPath);
4883
3846
  const outputPath = resolveWithinProject(project.dir, outputAssetPath);
4884
3847
  if (!outputPath) return c.json({ error: "forbidden" }, 403);
4885
- if (inputIsVideo && !VIDEO_OUTPUT_EXTENSIONS.has(extname3(outputAssetPath).toLowerCase())) {
3848
+ if (inputIsVideo && !VIDEO_OUTPUT_EXTENSIONS.has(extname2(outputAssetPath).toLowerCase())) {
4886
3849
  return c.json({ error: "video background removal output must be .webm or .mov" }, 400);
4887
3850
  }
4888
- if (inputIsImage && extname3(outputAssetPath).toLowerCase() !== ".png") {
3851
+ if (inputIsImage && extname2(outputAssetPath).toLowerCase() !== ".png") {
4889
3852
  return c.json({ error: "image background removal output must be .png" }, 400);
4890
3853
  }
4891
3854
  let backgroundOutputAssetPath;
@@ -5001,29 +3964,6 @@ function createStudioApi(adapter) {
5001
3964
  return api;
5002
3965
  }
5003
3966
 
5004
- // src/helpers/screenshotClip.ts
5005
- function getElementScreenshotClip(selector, selectorIndex) {
5006
- const matches = Array.from(document.querySelectorAll(selector)).filter(
5007
- (el2) => el2 instanceof HTMLElement
5008
- );
5009
- const safeIndex = Math.max(0, Math.min(matches.length - 1, Math.floor(selectorIndex ?? 0)));
5010
- const el = matches[safeIndex] ?? null;
5011
- if (!(el instanceof HTMLElement)) return void 0;
5012
- const rect = el.getBoundingClientRect();
5013
- if (rect.width < 4 || rect.height < 4) return void 0;
5014
- const pad = 8;
5015
- const x = Math.max(0, rect.left - pad);
5016
- const y = Math.max(0, rect.top - pad);
5017
- const maxWidth = window.innerWidth - x;
5018
- const maxHeight = window.innerHeight - y;
5019
- return {
5020
- x,
5021
- y,
5022
- width: Math.max(1, Math.min(rect.width + pad * 2, maxWidth)),
5023
- height: Math.max(1, Math.min(rect.height + pad * 2, maxHeight))
5024
- };
5025
- }
5026
-
5027
3967
  // src/helpers/backgroundRemovalJob.ts
5028
3968
  function createBackgroundRemovalJob(opts, render) {
5029
3969
  const state = {
@@ -5082,12 +4022,14 @@ export {
5082
4022
  STUDIO_MANUAL_EDITS_PATH,
5083
4023
  STUDIO_MOTION_PATH,
5084
4024
  buildSubCompositionHtml,
4025
+ consumeFileWriteReceipt,
5085
4026
  createBackgroundRemovalJob,
5086
4027
  createProjectSignature,
5087
4028
  createStudioApi,
5088
4029
  createStudioManualEditsRenderBodyScript,
5089
4030
  createStudioMotionRenderBodyScript,
5090
4031
  createStudioPositionSeekReapplyScript,
4032
+ fileContentVersion,
5091
4033
  getElementScreenshotClip,
5092
4034
  getMimeType,
5093
4035
  isSafePath,