@hyperframes/studio-server 0.7.60 → 0.7.62

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 (41) 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-7Q5AFHU6.js +279 -0
  6. package/dist/chunk-7Q5AFHU6.js.map +1 -0
  7. package/dist/chunk-IVOJZ24X.js +457 -0
  8. package/dist/chunk-IVOJZ24X.js.map +1 -0
  9. package/dist/chunk-OK7FKBKI.js +66 -0
  10. package/dist/chunk-OK7FKBKI.js.map +1 -0
  11. package/dist/chunk-VPA335OG.js +437 -0
  12. package/dist/chunk-VPA335OG.js.map +1 -0
  13. package/dist/chunk-W2SBTCO2.js +32 -0
  14. package/dist/chunk-W2SBTCO2.js.map +1 -0
  15. package/dist/chunk-X62ASOGO.js +30 -0
  16. package/dist/chunk-X62ASOGO.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.d.ts +3 -0
  33. package/dist/helpers/sourceMutation.js +9 -419
  34. package/dist/helpers/sourceMutation.js.map +1 -1
  35. package/dist/helpers/studioMotionRenderScript.js +4 -186
  36. package/dist/helpers/studioMotionRenderScript.js.map +1 -1
  37. package/dist/index.d.ts +6 -202
  38. package/dist/index.js +678 -1423
  39. package/dist/index.js.map +1 -1
  40. package/dist/mediaProxyPreview-ChE1ATG4.d.ts +259 -0
  41. 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-VPA335OG.js";
14
+ import {
15
+ getElementScreenshotClip
16
+ } from "./chunk-W2SBTCO2.js";
17
+ import {
18
+ injectMediaCodecMap,
19
+ isAutoProxyEnabled,
20
+ proxyEtagSalt,
21
+ resolvePreviewMediaCodecProbeCache
22
+ } from "./chunk-OK7FKBKI.js";
23
+ import {
24
+ ProxyCapacityError,
25
+ ProxyTranscodeError,
26
+ resolveProxy
27
+ } from "./chunk-IVOJZ24X.js";
28
+ import {
29
+ PROXY_VARIANT_CONFIG,
30
+ decideMediaProxyEligibility,
31
+ isProxyVariantRequest,
32
+ probeAssetCodec,
33
+ probeMediaMetadata,
34
+ resolveProxyVariantRequest
35
+ } from "./chunk-7Q5AFHU6.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
 
@@ -289,10 +334,10 @@ function registerStoryboardRoutes(api, adapter) {
289
334
  import { bodyLimit } from "hono/body-limit";
290
335
  import {
291
336
  closeSync,
292
- existsSync as existsSync3,
337
+ existsSync as existsSync4,
293
338
  ftruncateSync,
294
339
  openSync,
295
- readFileSync as readFileSync4,
340
+ readFileSync as readFileSync5,
296
341
  writeFileSync as writeFileSync4,
297
342
  writeSync,
298
343
  mkdirSync as mkdirSync3,
@@ -302,7 +347,7 @@ import {
302
347
  renameSync,
303
348
  readdirSync as readdirSync4
304
349
  } from "fs";
305
- import { resolve as resolve3, dirname, join as join6 } from "path";
350
+ import { resolve as resolve3, dirname as dirname2, join as join6 } from "path";
306
351
 
307
352
  // src/helpers/mime.ts
308
353
  var MIME_TYPES = {
@@ -319,7 +364,13 @@ var MIME_TYPES = {
319
364
  ".webp": "image/webp",
320
365
  ".ico": "image/x-icon",
321
366
  ".mp4": "video/mp4",
367
+ ".m4v": "video/mp4",
322
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",
323
374
  ".webm": "video/webm",
324
375
  ".mp3": "audio/mpeg",
325
376
  ".wav": "audio/wav",
@@ -347,7 +398,8 @@ function isAudioFile(name) {
347
398
  // src/helpers/waveform.ts
348
399
  import { spawn } from "child_process";
349
400
  import { existsSync as existsSync2, writeFileSync, mkdirSync } from "fs";
350
- import { join as join3, resolve as resolve2 } from "path";
401
+ import { join as join3 } from "path";
402
+ import { findFfBinary } from "@hyperframes/parsers/ff-binaries";
351
403
  var SAMPLE_RATE = 4e3;
352
404
  var PEAK_COUNT = 4e3;
353
405
  var WAVEFORM_CACHE_VERSION = "v2";
@@ -370,15 +422,10 @@ function computePeaks(floats, count) {
370
422
  const maxPeak = Math.max(...peaks, 1e-3);
371
423
  return peaks.map((p) => p / maxPeak);
372
424
  }
373
- function ffmpegBinary() {
374
- const configured = process.env.HYPERFRAMES_FFMPEG_PATH?.trim();
375
- if (configured) return resolve2(configured);
376
- return "ffmpeg";
377
- }
378
425
  function decodeAudioPeaks(audioPath) {
379
426
  return new Promise((resolvePromise, reject) => {
380
427
  const proc = spawn(
381
- ffmpegBinary(),
428
+ findFfBinary("ffmpeg") ?? "ffmpeg",
382
429
  [
383
430
  "-i",
384
431
  audioPath,
@@ -430,7 +477,7 @@ import { spawnSync } from "child_process";
430
477
  import { mkdtempSync, rmSync, writeFileSync as writeFileSync2 } from "fs";
431
478
  import { tmpdir } from "os";
432
479
  import { basename, join as join4 } from "path";
433
- var VIDEO_EXT = /\.(mp4|webm|mov)$/i;
480
+ var VIDEO_EXT = /\.(mp4|webm|mov|mkv|avi|m4v|mxf|mts|m2ts|ts)$/i;
434
481
  var AUDIO_EXT = /\.(mp3|wav|ogg|m4a|aac)$/i;
435
482
  function validateUploadedMedia(filePath, runner = spawnSync) {
436
483
  const isVideo = VIDEO_EXT.test(filePath);
@@ -580,31 +627,6 @@ function consumeFileWriteReceipt(absPath) {
580
627
  return { path, version, writeToken };
581
628
  }
582
629
 
583
- // src/helpers/finiteMutation.ts
584
- function findUnsafeMutationValues(value, path = "body", options = {}) {
585
- if (value === null) {
586
- return options.allowNullPath?.(path) ? [] : [{ path, reason: "null" }];
587
- }
588
- if (typeof value === "number") {
589
- return Number.isFinite(value) ? [] : [{ path, reason: "non-finite-number" }];
590
- }
591
- if (!value || typeof value !== "object") return [];
592
- if (Array.isArray(value)) {
593
- return value.flatMap(
594
- (item, index) => findUnsafeMutationValues(item, `${path}[${index}]`, options)
595
- );
596
- }
597
- return Object.entries(value).flatMap(
598
- ([key, item]) => findUnsafeMutationValues(item, `${path}.${key}`, options)
599
- );
600
- }
601
- var DOM_PATCH_NULL_VALUE_PATH = /^body\.operations\[\d+\]\.value$/;
602
- function findUnsafeDomPatchValues(value) {
603
- return findUnsafeMutationValues(value, "body", {
604
- allowNullPath: (path) => DOM_PATCH_NULL_VALUE_PATH.test(path)
605
- });
606
- }
607
-
608
630
  // src/routes/files.ts
609
631
  import { classifyPropertyGroup } from "@hyperframes/parsers/gsap-constants";
610
632
  import { parseGsapScriptAcorn } from "@hyperframes/parsers/gsap-parser-acorn";
@@ -632,429 +654,196 @@ import {
632
654
  scalePositionsInScript,
633
655
  dedupePositionWritesInScript
634
656
  } from "@hyperframes/parsers/gsap-writer-acorn";
657
+ import { parseHTML as parseHTML2 } from "linkedom";
635
658
 
636
- // src/helpers/sourceMutation.ts
659
+ // src/helpers/compositionInsertion.ts
660
+ import { existsSync as existsSync3, readFileSync as readFileSync4, realpathSync } from "fs";
661
+ import { randomUUID as randomUUID2 } from "crypto";
662
+ import { dirname, relative as relative3, resolve as resolve2, sep } from "path";
637
663
  import { parseHTML } from "linkedom";
638
- import postcss from "postcss";
639
- import selectorParser from "postcss-selector-parser";
640
- import { isAllowedHtmlAttribute, isSafeAttributeValue } from "@hyperframes/core/html-attr-safety";
641
- import { ensureHfIds } from "@hyperframes/parsers/hf-ids";
642
-
643
- // src/helpers/sourceStyleMutation.ts
644
- function parseStyleDecls(style) {
645
- const props = /* @__PURE__ */ new Map();
646
- const order = [];
647
- let i = 0;
648
- while (i < style.length) {
649
- let depth = 0;
650
- let inSingle = false;
651
- let inDouble = false;
652
- const start = i;
653
- while (i < style.length) {
654
- const ch = style[i];
655
- if (ch === "'" && !inDouble) inSingle = !inSingle;
656
- else if (ch === '"' && !inSingle) inDouble = !inDouble;
657
- else if (!inSingle && !inDouble) {
658
- if (ch === "(") depth++;
659
- else if (ch === ")") depth = Math.max(0, depth - 1);
660
- else if (ch === ";" && depth === 0) break;
661
- }
662
- i++;
663
- }
664
- const decl = style.slice(start, i).trim();
665
- i++;
666
- if (!decl) continue;
667
- const colon = decl.indexOf(":");
668
- if (colon < 0) continue;
669
- const key = decl.slice(0, colon).trim();
670
- const val = decl.slice(colon + 1).trim();
671
- if (!key) continue;
672
- if (!props.has(key)) order.push(key);
673
- props.set(key, val);
674
- }
675
- return { props, order };
676
- }
677
- function serializeStyleDecls(props, order) {
678
- return order.map((k) => `${k}: ${props.get(k) ?? ""}`).filter((d) => d.trim()).join("; ");
679
- }
680
- function patchStyleAttrString(style, property, value) {
681
- const { props, order } = parseStyleDecls(style);
682
- if (value === null) {
683
- props.delete(property);
684
- const idx = order.indexOf(property);
685
- if (idx >= 0) order.splice(idx, 1);
686
- } else {
687
- if (!props.has(property)) order.push(property);
688
- props.set(property, value);
664
+ var CompositionInsertionError = class extends Error {
665
+ constructor(message, status) {
666
+ super(message);
667
+ this.status = status;
689
668
  }
690
- return serializeStyleDecls(props, order);
691
- }
692
-
693
- // src/helpers/sourceMutation.ts
694
- function parseSourceDocument(source) {
695
- const hasDocumentShell = /<!doctype|<html[\s>]/i.test(source);
696
- if (hasDocumentShell) {
697
- return { document: parseHTML(source).document, wrappedFragment: false };
698
- }
699
- return {
700
- document: parseHTML(`<!DOCTYPE html><html><head></head><body>${source}</body></html>`).document,
701
- wrappedFragment: true
702
- };
703
- }
704
- function duplicateCssRulesForId(document2, originalId, newId) {
705
- const idToken = `#${originalId}`;
706
- const transform = selectorParser((selectors) => {
707
- selectors.walkIds((node) => {
708
- if (node.value === originalId) node.value = newId;
709
- });
710
- });
711
- for (const styleEl of document2.querySelectorAll("style")) {
712
- const css = styleEl.textContent ?? "";
713
- let root;
714
- try {
715
- root = postcss.parse(css);
716
- } catch {
717
- continue;
718
- }
719
- const clones = [];
720
- root.walkRules((rule) => {
721
- if (!rule.selector.includes(idToken)) return;
722
- const newSelector = transform.processSync(rule.selector);
723
- if (newSelector === rule.selector) return;
724
- const clone = rule.clone({ selector: newSelector });
725
- clones.push(clone);
726
- });
727
- if (clones.length > 0) {
728
- for (const c of clones) root.append(c);
729
- styleEl.textContent = root.toString();
730
- }
731
- }
732
- }
733
- function querySelectorAllWithTemplates(root, selector) {
734
- const matches = Array.from(root.querySelectorAll(selector));
735
- if (matches.length > 0) return matches;
736
- const templates = Array.from(root.querySelectorAll("template"));
737
- for (const tmpl of templates) {
738
- const inner = querySelectorAllWithTemplates(tmpl, selector);
739
- if (inner.length > 0) return inner;
669
+ status;
670
+ };
671
+ function descendants(root, selector) {
672
+ const found = Array.from(root.querySelectorAll(selector));
673
+ for (const template of root.querySelectorAll("template")) {
674
+ found.push(...descendants(template, selector));
740
675
  }
741
- return [];
676
+ return [...new Set(found)];
742
677
  }
743
- function escapeCssAttrValue(value) {
744
- return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
678
+ function compositionRoot(source) {
679
+ const document = parseHTML(source).document;
680
+ const root = descendants(document, "[data-composition-id]")[0];
681
+ if (!root) throw new CompositionInsertionError("Composition source has no root", 400);
682
+ return { document, root };
745
683
  }
746
- function findByHfId(document2, hfId) {
747
- try {
748
- const matches = querySelectorAllWithTemplates(
749
- document2,
750
- `[data-hf-id="${escapeCssAttrValue(hfId)}"]`
751
- );
752
- if (matches.length > 1) {
753
- console.warn(
754
- `sourceMutation: data-hf-id "${hfId}" matched ${matches.length} elements; using the first. ids must be unique per document.`
755
- );
756
- }
757
- return matches[0] ?? null;
758
- } catch {
759
- return null;
684
+ function positiveAttribute(root, ...names) {
685
+ for (const name of names) {
686
+ const value = Number.parseFloat(root.getAttribute(name) ?? "");
687
+ if (Number.isFinite(value) && value > 0) return value;
760
688
  }
689
+ throw new CompositionInsertionError(`Composition source has no valid ${names[0]}`, 400);
761
690
  }
762
- function findTargetElement(document2, target) {
763
- if (target.hfId) {
764
- const el = findByHfId(document2, target.hfId);
765
- if (el) return el;
691
+ function canonicalProjectPath(projectDir, candidate) {
692
+ if (!candidate) {
693
+ throw new CompositionInsertionError("Composition source escapes the project", 400);
766
694
  }
767
- if (target.id) {
768
- const byId = document2.getElementById(target.id);
769
- if (byId) return byId;
695
+ if (!existsSync3(candidate)) {
696
+ throw new CompositionInsertionError("Composition source was not found", 404);
770
697
  }
771
- if (!target.selector) return null;
772
- try {
773
- const matches = querySelectorAllWithTemplates(document2, target.selector);
774
- return matches[target.selectorIndex ?? 0] ?? null;
775
- } catch {
776
- return null;
698
+ const canonical = realpathSync(candidate);
699
+ if (!isSafePath(realpathSync(projectDir), canonical)) {
700
+ throw new CompositionInsertionError("Composition source escapes the project", 400);
777
701
  }
702
+ return canonical;
778
703
  }
779
- function removeElementFromHtml(source, target) {
780
- const { document: document2, wrappedFragment } = parseSourceDocument(source);
781
- const element = findTargetElement(document2, target);
782
- if (!element) return source;
783
- element.remove();
784
- return wrappedFragment ? document2.body.innerHTML || "" : document2.toString();
785
- }
786
- function isHTMLElement(el) {
787
- const HTMLEl = el.ownerDocument?.defaultView?.HTMLElement;
788
- return HTMLEl ? el instanceof HTMLEl : el.nodeType === 1 && "style" in el;
789
- }
790
- function resolveOperationTarget(parent, op) {
791
- if (op.childSelector === void 0) return parent;
792
- try {
793
- const child = parent.querySelectorAll(op.childSelector)[op.childIndex ?? 0] ?? null;
794
- return child && isHTMLElement(child) ? child : null;
795
- } catch {
796
- return null;
704
+ function validateSourcePath(sourcePath) {
705
+ if (!sourcePath.trim() || sourcePath.includes("\0") || /^[a-z]+:/i.test(sourcePath)) {
706
+ throw new CompositionInsertionError("Invalid composition source path", 400);
797
707
  }
798
708
  }
799
- function patchElementInHtml(source, target, operations) {
800
- const { document: document2, wrappedFragment } = parseSourceDocument(source);
801
- const el = findTargetElement(document2, target);
802
- if (!el || !isHTMLElement(el)) return { html: source, matched: false };
803
- const htmlEl = el;
804
- const resolved = [];
805
- for (const op of operations) {
806
- const opTarget = resolveOperationTarget(htmlEl, op);
807
- if (!opTarget) return { html: source, matched: false };
808
- resolved.push({ op, target: opTarget });
809
- }
810
- for (const { op, target: opTarget } of resolved) {
811
- switch (op.type) {
812
- case "inline-style":
813
- {
814
- const raw = opTarget.getAttribute("style") ?? "";
815
- const patched = patchStyleAttrString(raw, op.property, op.value);
816
- opTarget.setAttribute("style", patched);
817
- }
818
- break;
819
- case "attribute":
820
- {
821
- const fullAttr = op.property.startsWith("data-") ? op.property : `data-${op.property}`;
822
- if (op.value != null) {
823
- opTarget.setAttribute(fullAttr, op.value);
824
- } else {
825
- opTarget.removeAttribute(fullAttr);
826
- }
827
- }
828
- break;
829
- case "html-attribute":
830
- if (!isAllowedHtmlAttribute(op.property)) break;
831
- if (op.value != null) {
832
- if (!isSafeAttributeValue(op.property, op.value)) break;
833
- opTarget.setAttribute(op.property, op.value);
834
- } else {
835
- opTarget.removeAttribute(op.property);
836
- }
837
- break;
838
- case "text-content":
839
- if (op.value != null) {
840
- const inner = opTarget.children.length === 1 ? opTarget.firstElementChild : null;
841
- const textTarget = inner && isHTMLElement(inner) ? inner : opTarget;
842
- textTarget.textContent = op.value;
843
- }
844
- break;
845
- }
846
- }
847
- return {
848
- html: wrappedFragment ? document2.body.innerHTML || "" : document2.toString(),
849
- matched: true
850
- };
709
+ function canonicalProjectFile(projectDir, sourcePath) {
710
+ validateSourcePath(sourcePath);
711
+ return canonicalProjectPath(projectDir, resolveWithinProject(projectDir, sourcePath));
851
712
  }
852
- function probeElementInSource(source, target) {
853
- if (!target.id && !target.hfId && !target.selector) return false;
854
- const { document: document2 } = parseSourceDocument(source);
855
- const el = findTargetElement(document2, target);
856
- return el != null && isHTMLElement(el);
857
- }
858
- function resolveElementTiming(el) {
859
- const start = parseFloat(el.getAttribute("data-start") ?? "0") || 0;
860
- const usesDataEnd = el.hasAttribute("data-end");
861
- const duration = usesDataEnd ? parseFloat(el.getAttribute("data-end") ?? "") - start || 0 : parseFloat(el.getAttribute("data-duration") ?? "0") || 0;
862
- return { start, duration, usesDataEnd };
863
- }
864
- function setElementDuration(el, start, duration, usesDataEnd) {
865
- if (usesDataEnd) {
866
- const endTime = String(Math.round((start + duration) * 1e3) / 1e3);
867
- el.setAttribute("data-end", endTime);
868
- el.removeAttribute("data-duration");
869
- } else {
870
- el.setAttribute("data-duration", String(Math.round(duration * 1e3) / 1e3));
871
- el.removeAttribute("data-end");
872
- }
713
+ function canonicalDependency(projectDir, ownerAbs, sourcePath) {
714
+ validateSourcePath(sourcePath);
715
+ return canonicalProjectPath(
716
+ projectDir,
717
+ resolveWithinProject(projectDir, relative3(projectDir, resolve2(dirname(ownerAbs), sourcePath)))
718
+ );
873
719
  }
874
- function splitElementInHtml(source, target, splitTime, newId, fallbackTiming) {
875
- const { document: document2, wrappedFragment } = parseSourceDocument(source);
876
- const el = findTargetElement(document2, target);
877
- if (!el || !isHTMLElement(el)) return { html: source, matched: false, newId: null };
878
- const timing = resolveElementTiming(el);
879
- const { usesDataEnd } = timing;
880
- let { start, duration } = timing;
881
- if (duration <= 0 && fallbackTiming && fallbackTiming.duration > 0) {
882
- start = fallbackTiming.start;
883
- duration = fallbackTiming.duration;
884
- }
885
- if (duration <= 0 || splitTime <= start || splitTime >= start + duration) {
886
- return { html: source, matched: false, newId: null };
887
- }
888
- if (document2.getElementById(newId)) {
889
- let suffix = 2;
890
- const base = newId;
891
- while (document2.getElementById(newId)) {
892
- newId = `${base}-${suffix++}`;
720
+ function validateDependencyGraph(projectDir, targetAbs, sourceAbs) {
721
+ const visited = /* @__PURE__ */ new Set();
722
+ const visiting = /* @__PURE__ */ new Set();
723
+ const visit = (file) => {
724
+ if (file === targetAbs) {
725
+ throw new CompositionInsertionError("Composition insertion would create a cycle", 400);
726
+ }
727
+ if (visiting.has(file)) {
728
+ throw new CompositionInsertionError("Composition dependency cycle detected", 400);
729
+ }
730
+ if (visited.has(file)) return;
731
+ visiting.add(file);
732
+ const source = readFileSync4(file, "utf-8");
733
+ const { document } = compositionRoot(source);
734
+ for (const host of descendants(document, "[data-composition-src]")) {
735
+ const dependency = host.getAttribute("data-composition-src");
736
+ if (dependency) {
737
+ visit(canonicalDependency(projectDir, file, dependency));
738
+ }
893
739
  }
894
- }
895
- const firstDuration = splitTime - start;
896
- const secondDuration = duration - firstDuration;
897
- const clone = el.cloneNode(true);
898
- if (!isHTMLElement(clone)) return { html: source, matched: false, newId: null };
899
- clone.setAttribute("id", newId);
900
- clone.removeAttribute("data-hf-id");
901
- for (const node of clone.querySelectorAll("[data-hf-id]")) node.removeAttribute("data-hf-id");
902
- clone.setAttribute("data-start", String(Math.round(splitTime * 1e3) / 1e3));
903
- setElementDuration(clone, splitTime, secondDuration, usesDataEnd);
904
- const playbackStartAttr = el.hasAttribute("data-playback-start") ? "data-playback-start" : el.hasAttribute("data-media-start") ? "data-media-start" : null;
905
- if (playbackStartAttr) {
906
- const currentTrim = parseFloat(el.getAttribute(playbackStartAttr) ?? "0") || 0;
907
- const rateRaw = parseFloat(el.getAttribute("data-playback-rate") ?? "");
908
- const rate = Number.isFinite(rateRaw) ? rateRaw : 1;
909
- clone.setAttribute(
910
- playbackStartAttr,
911
- String(Math.round((currentTrim + firstDuration * rate) * 1e3) / 1e3)
912
- );
913
- }
914
- const originalId = el.getAttribute("id");
915
- if (originalId) {
916
- duplicateCssRulesForId(document2, originalId, newId);
917
- }
918
- el.setAttribute("data-start", String(Math.round(start * 1e3) / 1e3));
919
- setElementDuration(el, start, firstDuration, usesDataEnd);
920
- if (el.nextSibling) {
921
- el.parentElement.insertBefore(clone, el.nextSibling);
922
- } else {
923
- el.parentElement.appendChild(clone);
924
- }
925
- const html = wrappedFragment ? document2.body.innerHTML || "" : document2.toString();
926
- return {
927
- // The split owns its new nodes' stable ids. Leaving the clone unstamped makes
928
- // the next preview request persist different bytes after history is recorded.
929
- html: ensureHfIds(html),
930
- matched: true,
931
- newId
740
+ visiting.delete(file);
741
+ visited.add(file);
932
742
  };
743
+ visit(sourceAbs);
933
744
  }
934
- function getInlineStylePx(el, property) {
935
- const style = (isHTMLElement(el) ? el.getAttribute("style") : null) ?? "";
936
- const { props } = parseStyleDecls(style);
937
- const raw = props.get(property);
938
- if (!raw) return 0;
939
- const n = parseFloat(raw);
940
- return Number.isFinite(n) ? n : 0;
941
- }
942
- function setInlineLeftTop(el, left, top) {
943
- let style = el.getAttribute("style") ?? "";
944
- style = patchStyleAttrString(style, "left", `${left}px`);
945
- style = patchStyleAttrString(style, "top", `${top}px`);
946
- el.setAttribute("style", style);
947
- }
948
- function uniqueGroupDomId(document2, groupId) {
949
- const base = groupId.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "group";
950
- let id = base;
951
- let n = 2;
952
- while (document2.getElementById(id)) {
953
- id = `${base}-${n}`;
954
- n += 1;
955
- }
956
- return id;
745
+ function numberAttribute(element, name, fallback = 0) {
746
+ const value = Number.parseFloat(element.getAttribute(name) ?? "");
747
+ return Number.isFinite(value) ? value : fallback;
957
748
  }
958
- function wrapElementsInHtml(source, targets, groupId, bbox, rebases) {
959
- const { document: document2, wrappedFragment } = parseSourceDocument(source);
960
- if (targets.length === 0) {
961
- return { html: source, matched: false, groupId: null, error: "no targets" };
962
- }
963
- const els = [];
964
- const seen = /* @__PURE__ */ new Set();
965
- for (const target of targets) {
966
- const el = findTargetElement(document2, target);
967
- if (!el || !isHTMLElement(el) || seen.has(el)) continue;
968
- seen.add(el);
969
- els.push(el);
970
- }
971
- if (els.length === 0) {
972
- return { html: source, matched: false, groupId: null, error: "no targets matched" };
973
- }
974
- const parent = els[0]?.parentElement;
975
- if (!parent || els.some((el) => el.parentElement !== parent)) {
976
- return {
977
- html: source,
978
- matched: false,
979
- groupId: null,
980
- error: "grouped elements must share a single parent"
981
- };
982
- }
983
- const memberSet = new Set(els);
984
- const ordered = Array.from(parent.children).filter((c) => memberSet.has(c));
985
- const rebaseByEl = /* @__PURE__ */ new Map();
986
- for (const rebase of rebases) {
987
- const el = findTargetElement(document2, rebase.target);
988
- if (el) rebaseByEl.set(el, { left: rebase.left, top: rebase.top });
989
- }
990
- const wrapper = document2.createElement("div");
991
- wrapper.setAttribute("data-hf-group", groupId);
992
- wrapper.setAttribute("id", uniqueGroupDomId(document2, groupId));
993
- const memberZIndexes = ordered.map(
994
- (el) => Number.parseInt(
995
- parseStyleDecls(el.getAttribute("style") ?? "").props.get("z-index") ?? "",
996
- 10
997
- )
998
- ).filter((z) => Number.isFinite(z));
999
- const maxZ = memberZIndexes.length > 0 ? Math.max(...memberZIndexes) : null;
1000
- wrapper.setAttribute(
749
+ function rangesOverlap(start, duration, other) {
750
+ const otherStart = numberAttribute(other, "data-start");
751
+ const otherDuration = numberAttribute(other, "data-duration");
752
+ return start < otherStart + otherDuration && otherStart < start + duration;
753
+ }
754
+ function resolveTrack(root, desiredTrack, start, duration) {
755
+ const clips = descendants(root, "[data-start][data-duration]").filter(
756
+ (element) => element !== root && element.parentElement?.closest("[data-composition-id]") === root
757
+ );
758
+ const tracks = [...new Set(clips.map((clip) => numberAttribute(clip, "data-track-index")))].sort(
759
+ (a, b) => a - b
760
+ );
761
+ const isFree = (track) => !clips.some(
762
+ (clip) => numberAttribute(clip, "data-track-index") === track && rangesOverlap(start, duration, clip)
763
+ );
764
+ if (isFree(desiredTrack)) return desiredTrack;
765
+ const row = tracks.indexOf(desiredTrack);
766
+ for (let index = row - 1; index >= 0; index--) {
767
+ const track = tracks[index];
768
+ if (track !== void 0 && isFree(track)) return track;
769
+ }
770
+ for (let index = Math.max(0, row + 1); index < tracks.length; index++) {
771
+ const track = tracks[index];
772
+ if (track !== void 0 && isFree(track)) return track;
773
+ }
774
+ return Math.max(desiredTrack, ...tracks, -1) + 1;
775
+ }
776
+ function uniqueHostId(root, base) {
777
+ const ids = /* @__PURE__ */ new Set([
778
+ ...descendants(root, "[id]").map((element) => element.id),
779
+ ...descendants(root, "[data-composition-id]").flatMap((element) => {
780
+ const id = element.getAttribute("data-composition-id");
781
+ return id ? [id] : [];
782
+ })
783
+ ]);
784
+ if (!ids.has(base)) return base;
785
+ let suffix = 2;
786
+ while (ids.has(`${base}_${suffix}`)) suffix += 1;
787
+ return `${base}_${suffix}`;
788
+ }
789
+ function relativeSourcePath(targetAbs, sourceAbs) {
790
+ return relative3(dirname(targetAbs), sourceAbs).split(sep).join("/");
791
+ }
792
+ function insertCompositionIntoSource(input) {
793
+ const targetAbs = canonicalProjectFile(input.projectDir, input.targetPath);
794
+ const sourceAbs = canonicalProjectFile(input.projectDir, input.sourcePath);
795
+ validateDependencyGraph(input.projectDir, targetAbs, sourceAbs);
796
+ const source = readFileSync4(sourceAbs, "utf-8");
797
+ const sourceComposition = compositionRoot(source).root;
798
+ const duration = positiveAttribute(
799
+ sourceComposition,
800
+ "data-composition-duration",
801
+ "data-duration"
802
+ );
803
+ const width = positiveAttribute(sourceComposition, "data-width");
804
+ const height = positiveAttribute(sourceComposition, "data-height");
805
+ const { document, root } = compositionRoot(input.parentSource);
806
+ const parentDuration = positiveAttribute(root, "data-duration", "data-composition-duration");
807
+ const base = (sourceComposition.getAttribute("data-composition-id") ?? "composition").replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "composition";
808
+ const hostId = uniqueHostId(root, base);
809
+ const track = resolveTrack(
810
+ root,
811
+ Math.max(0, Math.round(input.desiredTrack)),
812
+ input.start,
813
+ duration
814
+ );
815
+ const zIndex = Math.max(
816
+ 0,
817
+ ...descendants(root, "[style]").map((element) => {
818
+ const match = /(?:^|;)\s*z-index\s*:\s*(-?\d+)/i.exec(element.getAttribute("style") ?? "");
819
+ return match?.[1] ? Number.parseInt(match[1], 10) : 0;
820
+ })
821
+ ) + 1;
822
+ const host = document.createElement("div");
823
+ host.id = hostId;
824
+ host.className = "clip";
825
+ host.setAttribute("data-hf-id", `hf-${randomUUID2()}`);
826
+ host.setAttribute("data-composition-id", hostId);
827
+ host.setAttribute("data-composition-src", relativeSourcePath(targetAbs, sourceAbs));
828
+ host.setAttribute("data-start", String(Math.round(input.start * 100) / 100));
829
+ host.setAttribute("data-duration", String(duration));
830
+ host.setAttribute("data-playback-start", "0");
831
+ host.setAttribute("data-track-index", String(track));
832
+ host.setAttribute("data-width", String(width));
833
+ host.setAttribute("data-height", String(height));
834
+ host.setAttribute(
1001
835
  "style",
1002
- `position: absolute; left: ${bbox.left}px; top: ${bbox.top}px; width: ${bbox.width}px; height: ${bbox.height}px` + (maxZ !== null ? `; z-index: ${maxZ}` : "")
836
+ `position: absolute; left: 0px; top: 0px; width: ${width}px; height: ${height}px; z-index: ${zIndex}`
1003
837
  );
1004
- parent.insertBefore(wrapper, ordered[ordered.length - 1] ?? null);
1005
- for (const el of ordered) {
1006
- const rebase = rebaseByEl.get(el);
1007
- if (rebase) setInlineLeftTop(el, rebase.left, rebase.top);
1008
- wrapper.appendChild(el);
1009
- }
1010
- return {
1011
- html: wrappedFragment ? document2.body.innerHTML || "" : document2.toString(),
1012
- matched: true,
1013
- groupId
1014
- };
1015
- }
1016
- function unwrapElementsFromHtml(source, groupTarget) {
1017
- const { document: document2, wrappedFragment } = parseSourceDocument(source);
1018
- const group = findTargetElement(document2, groupTarget);
1019
- if (!group || !isHTMLElement(group)) return { html: source, unwrapped: false };
1020
- if (!group.hasAttribute("data-hf-group")) return { html: source, unwrapped: false };
1021
- const parent = group.parentElement;
1022
- if (!parent) return { html: source, unwrapped: false };
1023
- const wLeft = getInlineStylePx(group, "left");
1024
- const wTop = getInlineStylePx(group, "top");
1025
- const groupCenter = {
1026
- cx: wLeft + getInlineStylePx(group, "width") / 2,
1027
- cy: wTop + getInlineStylePx(group, "height") / 2
1028
- };
1029
- const members = [];
1030
- for (const child of Array.from(group.children)) {
1031
- if (isHTMLElement(child)) {
1032
- const newLeft = getInlineStylePx(child, "left") + wLeft;
1033
- const newTop = getInlineStylePx(child, "top") + wTop;
1034
- setInlineLeftTop(child, newLeft, newTop);
1035
- if (child.id) {
1036
- members.push({
1037
- id: child.id,
1038
- cx: newLeft + getInlineStylePx(child, "width") / 2,
1039
- cy: newTop + getInlineStylePx(child, "height") / 2
1040
- });
1041
- }
1042
- }
1043
- parent.insertBefore(child, group);
838
+ root.appendChild(host);
839
+ if (input.start + duration > parentDuration) {
840
+ const name = root.hasAttribute("data-duration") ? "data-duration" : "data-composition-duration";
841
+ root.setAttribute(name, String(Math.round((input.start + duration) * 100) / 100));
1044
842
  }
1045
- const groupId = group.id || void 0;
1046
- group.remove();
1047
- return {
1048
- html: wrappedFragment ? document2.body.innerHTML || "" : document2.toString(),
1049
- unwrapped: true,
1050
- unwrappedGroupId: groupId,
1051
- members,
1052
- groupCenter
1053
- };
843
+ return { html: document.toString(), hostId, track, duration };
1054
844
  }
1055
845
 
1056
846
  // src/routes/files.ts
1057
- import { parseHTML as parseHTML2 } from "linkedom";
1058
847
  function isAcornGsapWriterEnabled() {
1059
848
  const val = process.env["STUDIO_SDK_CUTOVER_ENABLED"];
1060
849
  return val === "true" || val === "1";
@@ -1076,7 +865,7 @@ async function resolveProjectPath(c, adapter, pathPrefix, opts) {
1076
865
  if (!absPath) {
1077
866
  return { error: c.json({ error: "forbidden" }, 403) };
1078
867
  }
1079
- if (opts?.mustExist && !existsSync3(absPath)) {
868
+ if (opts?.mustExist && !existsSync4(absPath)) {
1080
869
  return { error: c.json({ error: "not found" }, 404) };
1081
870
  }
1082
871
  return { project, filePath, absPath };
@@ -1087,6 +876,25 @@ function resolveProjectFile(c, adapter, opts) {
1087
876
  function resolveFileMutationContext(c, adapter, operation) {
1088
877
  return resolveProjectPath(c, adapter, (id) => `/projects/${id}/file-mutations/${operation}/`);
1089
878
  }
879
+ function isAtomicCutTarget(value) {
880
+ if (!value || typeof value !== "object") return false;
881
+ const target = value;
882
+ return !!target.target && typeof target.target === "object" && Number.isFinite(target.splitTime) && Number.isFinite(target.elementStart) && Number.isFinite(target.elementDuration) && Number(target.elementDuration) > 0;
883
+ }
884
+ function isAtomicCutFileRequest(value) {
885
+ if (!value || typeof value !== "object") return false;
886
+ const file = value;
887
+ return typeof file.path === "string" && file.path.length > 0 && typeof file.expectedVersion === "string" && Array.isArray(file.targets) && file.targets.length > 0 && file.targets.every(isAtomicCutTarget);
888
+ }
889
+ var atomicCutTail = Promise.resolve();
890
+ function serializeAtomicCut(task) {
891
+ const next = atomicCutTail.then(task, task);
892
+ atomicCutTail = next.then(
893
+ () => void 0,
894
+ () => void 0
895
+ );
896
+ return next;
897
+ }
1090
898
  function isElementPatchRequest(value) {
1091
899
  if (typeof value !== "object" || value === null) return false;
1092
900
  if (!("target" in value) || typeof value.target !== "object" || value.target === null) {
@@ -1122,7 +930,7 @@ function commitElementPatchBatches(projectDir, batches, writeFile = writeFileSyn
1122
930
  resolvedPaths.add(absPath);
1123
931
  let before;
1124
932
  try {
1125
- before = readFileSync4(absPath, "utf-8");
933
+ before = readFileSync5(absPath, "utf-8");
1126
934
  } catch {
1127
935
  return { error: "not-found", sourceFile: batch.sourceFile };
1128
936
  }
@@ -1234,8 +1042,8 @@ async function parseMutationBody(c) {
1234
1042
  return { target: body.target, body };
1235
1043
  }
1236
1044
  function ensureDir(filePath) {
1237
- const dir = dirname(filePath);
1238
- if (!existsSync3(dir)) mkdirSync3(dir, { recursive: true });
1045
+ const dir = dirname2(filePath);
1046
+ if (!existsSync4(dir)) mkdirSync3(dir, { recursive: true });
1239
1047
  }
1240
1048
  function generateCopyPath(projectDir, originalPath) {
1241
1049
  const ext = originalPath.includes(".") ? "." + originalPath.split(".").pop() : "";
@@ -1244,7 +1052,7 @@ function generateCopyPath(projectDir, originalPath) {
1244
1052
  const cleanBase = copyMatch ? base.slice(0, -copyMatch[0].length) : base;
1245
1053
  let num = copyMatch ? copyMatch[1] ? parseInt(copyMatch[1]) + 1 : 2 : 1;
1246
1054
  let candidate = num === 1 ? `${cleanBase} (copy)${ext}` : `${cleanBase} (copy ${num})${ext}`;
1247
- while (existsSync3(resolve3(projectDir, candidate))) {
1055
+ while (existsSync4(resolve3(projectDir, candidate))) {
1248
1056
  num++;
1249
1057
  candidate = `${cleanBase} (copy ${num})${ext}`;
1250
1058
  }
@@ -1255,7 +1063,7 @@ function walkFiles(dir, filter) {
1255
1063
  for (const entry of readdirSync4(dir, { withFileTypes: true })) {
1256
1064
  const full = join6(dir, entry.name);
1257
1065
  if (entry.isDirectory()) {
1258
- if (entry.name === "node_modules" || entry.name === ".thumbnails" || entry.name === "renders")
1066
+ if (entry.name === "node_modules" || entry.name === ".thumbnails" || entry.name === "renders" || entry.name === ".transcode-cache")
1259
1067
  continue;
1260
1068
  results.push(...walkFiles(full, filter));
1261
1069
  } else if (filter(entry.name)) {
@@ -1271,7 +1079,7 @@ function updateReferences(projectDir, oldPath, newPath) {
1271
1079
  );
1272
1080
  let updatedCount = 0;
1273
1081
  for (const file of textFiles) {
1274
- const content = readFileSync4(file, "utf-8");
1082
+ const content = readFileSync5(file, "utf-8");
1275
1083
  if (!content.includes(oldPath)) continue;
1276
1084
  const updated = content.split(oldPath).join(newPath);
1277
1085
  if (updated !== content) {
@@ -1282,10 +1090,10 @@ function updateReferences(projectDir, oldPath, newPath) {
1282
1090
  return updatedCount;
1283
1091
  }
1284
1092
  function extractGsapScriptBlock(html) {
1285
- const { document: document2 } = parseHTML2(html);
1093
+ const { document } = parseHTML2(html);
1286
1094
  const scripts = [
1287
- ...document2.querySelectorAll("script:not([src])"),
1288
- ...Array.from(document2.querySelectorAll("template")).flatMap(
1095
+ ...document.querySelectorAll("script:not([src])"),
1096
+ ...Array.from(document.querySelectorAll("template")).flatMap(
1289
1097
  (tmpl) => Array.from(tmpl.querySelectorAll("script:not([src])"))
1290
1098
  )
1291
1099
  ];
@@ -1294,10 +1102,10 @@ function extractGsapScriptBlock(html) {
1294
1102
  if (content.includes("gsap.timeline") || content.includes(".set(") || content.includes(".to(")) {
1295
1103
  return {
1296
1104
  scriptText: content,
1297
- document: document2,
1105
+ document,
1298
1106
  replaceScript(newText) {
1299
1107
  script.textContent = newText;
1300
- return document2.toString();
1108
+ return document.toString();
1301
1109
  }
1302
1110
  };
1303
1111
  }
@@ -1395,11 +1203,11 @@ function bakeGroupTransformIntoMembers(html, groupId, members, groupCenter) {
1395
1203
  }
1396
1204
  return block.replaceScript(script);
1397
1205
  }
1398
- function stripStudioEditsFromTarget(document2, selector) {
1206
+ function stripStudioEditsFromTarget(document, selector) {
1399
1207
  if (!selector) return 0;
1400
1208
  let stripped = 0;
1401
1209
  try {
1402
- for (const el of document2.querySelectorAll(selector)) {
1210
+ for (const el of document.querySelectorAll(selector)) {
1403
1211
  if (!isHTMLElement(el)) continue;
1404
1212
  const htmlEl = el;
1405
1213
  let touched = false;
@@ -1469,11 +1277,11 @@ function resolveFinalOpacity(anim) {
1469
1277
  const num = Number(raw);
1470
1278
  return Number.isFinite(num) && num !== 0 ? num : null;
1471
1279
  }
1472
- function bakeVisibilityOnDelete(document2, anim) {
1280
+ function bakeVisibilityOnDelete(document, anim) {
1473
1281
  const opacity = resolveFinalOpacity(anim);
1474
1282
  if (opacity === null) return;
1475
1283
  try {
1476
- for (const el of document2.querySelectorAll(anim.targetSelector)) {
1284
+ for (const el of document.querySelectorAll(anim.targetSelector)) {
1477
1285
  if (isHTMLElement(el)) el.style.setProperty("opacity", String(opacity));
1478
1286
  }
1479
1287
  } catch {
@@ -1528,7 +1336,7 @@ function validateGsapMutationRequest(c, body) {
1528
1336
  return null;
1529
1337
  }
1530
1338
  async function prepareGsapMutationScript(c, res, firstMutation) {
1531
- const beforeHtml = readFileSync4(res.absPath, "utf-8");
1339
+ const beforeHtml = readFileSync5(res.absPath, "utf-8");
1532
1340
  let html = beforeHtml;
1533
1341
  let block = extractGsapScriptBlock(html);
1534
1342
  if (!block && (firstMutation.type === "add" || firstMutation.type === "add-with-keyframes")) {
@@ -1588,7 +1396,7 @@ async function applyGsapMutations(c, res, mutations) {
1588
1396
  const changed = block.scriptText !== initialScript;
1589
1397
  const newHtml = changed ? block.replaceScript(block.scriptText) : html;
1590
1398
  let backupPath = null;
1591
- if (readFileSync4(res.absPath, "utf-8") !== beforeHtml) {
1399
+ if (readFileSync5(res.absPath, "utf-8") !== beforeHtml) {
1592
1400
  return c.json({ error: "file changed during GSAP mutation", conflict: true }, 409);
1593
1401
  }
1594
1402
  if (changed) {
@@ -2211,6 +2019,74 @@ async function executeGsapMutationRecast(body, block, respond) {
2211
2019
  return respond({ error: `unknown mutation type: ${body.type}` }, 400);
2212
2020
  }
2213
2021
  }
2022
+ async function foldAtomicCutFile(c, file, absPath, before) {
2023
+ let after = before;
2024
+ let splitCount = 0;
2025
+ const skippedSelectors = /* @__PURE__ */ new Set();
2026
+ const respond = (data, status) => status ? c.json(data, status) : c.json(data);
2027
+ const orderedTargets = file.targets.map((cut, index) => ({ cut, index })).sort((left, right) => {
2028
+ const locatorKey = (entry) => !entry.target.id && !entry.target.hfId && entry.target.selector ? entry.target.selector : null;
2029
+ const leftKey = locatorKey(left.cut);
2030
+ const rightKey = locatorKey(right.cut);
2031
+ if (leftKey && rightKey) {
2032
+ return leftKey.localeCompare(rightKey) || (right.cut.target.selectorIndex ?? 0) - (left.cut.target.selectorIndex ?? 0);
2033
+ }
2034
+ if (leftKey) return -1;
2035
+ if (rightKey) return 1;
2036
+ return left.index - right.index;
2037
+ }).map(({ cut }) => cut);
2038
+ for (const cut of orderedTargets) {
2039
+ const baseId = cut.originalId || cut.target.id || "clip";
2040
+ const split = splitElementInHtml(after, cut.target, cut.splitTime, `${baseId}-split`, {
2041
+ start: cut.elementStart,
2042
+ duration: cut.elementDuration,
2043
+ playbackStart: cut.playbackStart,
2044
+ playbackRate: cut.playbackRate,
2045
+ stampPlaybackStart: cut.isComposition
2046
+ });
2047
+ if (!split.matched || !split.newId) {
2048
+ return c.json(
2049
+ { error: `Cut target was not found or was outside its authored bounds in ${file.path}` },
2050
+ 400
2051
+ );
2052
+ }
2053
+ after = split.html;
2054
+ splitCount++;
2055
+ if (!cut.originalId) continue;
2056
+ const block = extractGsapScriptBlock(after);
2057
+ if (!block) continue;
2058
+ const result = await executeGsapMutation(
2059
+ {
2060
+ type: "split-animations",
2061
+ originalId: cut.originalId,
2062
+ newId: split.newId,
2063
+ splitTime: cut.splitTime,
2064
+ elementStart: cut.elementStart,
2065
+ elementDuration: cut.elementDuration
2066
+ },
2067
+ block,
2068
+ respond
2069
+ );
2070
+ if (result instanceof Response) return result;
2071
+ let script = typeof result === "string" ? result : result.script;
2072
+ if (typeof result !== "string") {
2073
+ for (const selector of result.skippedSelectors) skippedSelectors.add(selector);
2074
+ }
2075
+ if (script !== block.scriptText) {
2076
+ const parser = await loadGsapParser();
2077
+ script = parser.syncPositionHoldsBeforeKeyframes(script);
2078
+ after = block.replaceScript(script);
2079
+ }
2080
+ }
2081
+ return {
2082
+ path: file.path,
2083
+ absPath,
2084
+ before,
2085
+ after,
2086
+ splitCount,
2087
+ skippedSelectors: [...skippedSelectors]
2088
+ };
2089
+ }
2214
2090
  async function processUploadedFiles(formData, targetDir, projectDir) {
2215
2091
  const MAX_UPLOAD_BYTES = 500 * 1024 * 1024;
2216
2092
  const uploaded = [];
@@ -2230,13 +2106,13 @@ async function processUploadedFiles(formData, targetDir, projectDir) {
2230
2106
  if (!isSafePath(projectDir, destPath)) continue;
2231
2107
  let finalPath = destPath;
2232
2108
  let finalName = name;
2233
- if (existsSync3(finalPath)) {
2109
+ if (existsSync4(finalPath)) {
2234
2110
  const dotIdx = name.indexOf(".", name.startsWith(".") ? 1 : 0);
2235
2111
  const ext = dotIdx > 0 ? name.slice(dotIdx) : "";
2236
2112
  const base = dotIdx > 0 ? name.slice(0, dotIdx) : name;
2237
2113
  let n = 2;
2238
2114
  const MAX_COPY_INDEX = 1e4;
2239
- while (n < MAX_COPY_INDEX && existsSync3(resolve3(targetDir, `${base} (${n})${ext}`))) n++;
2115
+ while (n < MAX_COPY_INDEX && existsSync4(resolve3(targetDir, `${base} (${n})${ext}`))) n++;
2240
2116
  if (n >= MAX_COPY_INDEX) {
2241
2117
  skipped.push(name);
2242
2118
  continue;
@@ -2264,13 +2140,13 @@ function registerFileRoutes(api, adapter) {
2264
2140
  api.get("/projects/:id/files/*", async (c) => {
2265
2141
  const res = await resolveProjectFile(c, adapter);
2266
2142
  if ("error" in res) return res.error;
2267
- if (!existsSync3(res.absPath)) {
2143
+ if (!existsSync4(res.absPath)) {
2268
2144
  if (c.req.query("optional") === "1") {
2269
2145
  return c.json({ filename: res.filePath, content: "" });
2270
2146
  }
2271
2147
  return c.json({ error: "not found" }, 404);
2272
2148
  }
2273
- const content = readFileSync4(res.absPath, "utf-8");
2149
+ const content = readFileSync5(res.absPath, "utf-8");
2274
2150
  const version = fileContentVersion(content);
2275
2151
  c.header("ETag", version);
2276
2152
  return c.json({ filename: res.filePath, content, version });
@@ -2284,7 +2160,7 @@ function registerFileRoutes(api, adapter) {
2284
2160
  if (expectedVersion === null && !createOnly) {
2285
2161
  let currentContent = null;
2286
2162
  try {
2287
- currentContent = readFileSync4(res.absPath, "utf-8");
2163
+ currentContent = readFileSync5(res.absPath, "utf-8");
2288
2164
  } catch (error) {
2289
2165
  if (!error || typeof error !== "object" || !("code" in error) || error.code !== "ENOENT") {
2290
2166
  throw error;
@@ -2310,7 +2186,7 @@ function registerFileRoutes(api, adapter) {
2310
2186
  if (!error || typeof error !== "object" || !("code" in error) || error.code !== "EEXIST") {
2311
2187
  throw error;
2312
2188
  }
2313
- const currentContent = readFileSync4(res.absPath, "utf-8");
2189
+ const currentContent = readFileSync5(res.absPath, "utf-8");
2314
2190
  return c.json(
2315
2191
  {
2316
2192
  error: "file conflict",
@@ -2345,7 +2221,7 @@ function registerFileRoutes(api, adapter) {
2345
2221
  );
2346
2222
  }
2347
2223
  try {
2348
- const currentContent = readFileSync4(fd, "utf-8");
2224
+ const currentContent = readFileSync5(fd, "utf-8");
2349
2225
  const currentVersion = fileContentVersion(currentContent);
2350
2226
  if (expectedVersion !== currentVersion) {
2351
2227
  return c.json(
@@ -2382,7 +2258,7 @@ function registerFileRoutes(api, adapter) {
2382
2258
  api.post("/projects/:id/files/*", async (c) => {
2383
2259
  const res = await resolveProjectFile(c, adapter);
2384
2260
  if ("error" in res) return res.error;
2385
- if (existsSync3(res.absPath)) {
2261
+ if (existsSync4(res.absPath)) {
2386
2262
  return c.json({ error: "already exists" }, 409);
2387
2263
  }
2388
2264
  ensureDir(res.absPath);
@@ -2406,15 +2282,71 @@ function registerFileRoutes(api, adapter) {
2406
2282
  backupPath: backupPathForResponse(res.project.dir, backup.backupPath)
2407
2283
  });
2408
2284
  });
2285
+ api.post("/projects/:id/file-mutations/insert-composition/*", async (c) => {
2286
+ const ctx = await resolveFileMutationContext(c, adapter, "insert-composition");
2287
+ if ("error" in ctx) return ctx.error;
2288
+ const body = await c.req.json().catch(() => null);
2289
+ if (!body || typeof body.sourcePath !== "string" || typeof body.start !== "number" || !Number.isFinite(body.start) || body.start < 0 || typeof body.track !== "number" || !Number.isFinite(body.track) || typeof body.expectedVersion !== "string") {
2290
+ return c.json({ error: "sourcePath, finite placement, and expectedVersion required" }, 400);
2291
+ }
2292
+ let before;
2293
+ try {
2294
+ before = readFileSync5(ctx.absPath, "utf-8");
2295
+ } catch (error) {
2296
+ if (!error || typeof error !== "object" || !("code" in error) || error.code !== "ENOENT") {
2297
+ throw error;
2298
+ }
2299
+ return c.json({ error: "not found" }, 404);
2300
+ }
2301
+ const currentVersion = fileContentVersion(before);
2302
+ if (body.expectedVersion !== currentVersion) {
2303
+ return c.json({ error: "file conflict", currentVersion, currentContent: before }, 409);
2304
+ }
2305
+ let insertion;
2306
+ try {
2307
+ insertion = insertCompositionIntoSource({
2308
+ projectDir: ctx.project.dir,
2309
+ targetPath: ctx.filePath,
2310
+ sourcePath: body.sourcePath,
2311
+ parentSource: before,
2312
+ start: body.start,
2313
+ desiredTrack: body.track
2314
+ });
2315
+ } catch (error) {
2316
+ if (error instanceof CompositionInsertionError) {
2317
+ return c.json({ error: error.message }, error.status);
2318
+ }
2319
+ throw error;
2320
+ }
2321
+ const backup = snapshotBeforeWrite(ctx.project.dir, ctx.absPath);
2322
+ if (backup.error) return c.json({ error: `backup failed: ${backup.error}` }, 500);
2323
+ writeFileSync4(ctx.absPath, insertion.html, "utf-8");
2324
+ const version = fileContentVersion(insertion.html);
2325
+ const writeToken = createWriteToken(c.req.header("X-Hyperframes-Write-Token"));
2326
+ recordFileWriteReceipt(ctx.absPath, { path: ctx.filePath, version, writeToken });
2327
+ c.header("ETag", version);
2328
+ return c.json({
2329
+ ok: true,
2330
+ path: ctx.filePath,
2331
+ hostId: insertion.hostId,
2332
+ track: insertion.track,
2333
+ duration: insertion.duration,
2334
+ before,
2335
+ after: insertion.html,
2336
+ version,
2337
+ writeToken,
2338
+ backupPath: backupPathForResponse(ctx.project.dir, backup.backupPath)
2339
+ });
2340
+ });
2409
2341
  api.post("/projects/:id/file-mutations/remove-element/*", async (c) => {
2410
2342
  const ctx = await resolveFileMutationContext(c, adapter, "remove-element");
2411
2343
  if ("error" in ctx) return ctx.error;
2412
- if (!existsSync3(ctx.absPath)) {
2344
+ if (!existsSync4(ctx.absPath)) {
2413
2345
  return c.json({ error: "not found" }, 404);
2414
2346
  }
2415
2347
  const parsed = await parseMutationBody(c);
2416
2348
  if ("error" in parsed) return parsed.error;
2417
- const originalContent = readFileSync4(ctx.absPath, "utf-8");
2349
+ const originalContent = readFileSync5(ctx.absPath, "utf-8");
2418
2350
  return writeIfChanged(
2419
2351
  c,
2420
2352
  ctx.project.dir,
@@ -2424,6 +2356,130 @@ function registerFileRoutes(api, adapter) {
2424
2356
  removeElementFromHtml(originalContent, parsed.target)
2425
2357
  );
2426
2358
  });
2359
+ api.post("/projects/:id/file-mutations/split-batch", async (c) => {
2360
+ const body = await c.req.json().catch(() => null);
2361
+ if (!Array.isArray(body?.files) || body.files.length === 0 || !body.files.every(isAtomicCutFileRequest)) {
2362
+ return c.json({ error: "files with path, expectedVersion, and cut targets required" }, 400);
2363
+ }
2364
+ const files = body.files;
2365
+ const project = await adapter.resolveProject(c.req.param("id"));
2366
+ if (!project) return c.json({ error: "not found" }, 404);
2367
+ return serializeAtomicCut(async () => {
2368
+ const seen = /* @__PURE__ */ new Set();
2369
+ const prepared = [];
2370
+ for (const file of files) {
2371
+ const absPath = resolveWithinProject(project.dir, file.path);
2372
+ if (!absPath) return c.json({ error: `forbidden path: ${file.path}` }, 403);
2373
+ if (seen.has(absPath)) return c.json({ error: `duplicate path: ${file.path}` }, 400);
2374
+ seen.add(absPath);
2375
+ let before;
2376
+ try {
2377
+ before = readFileSync5(absPath, "utf-8");
2378
+ } catch {
2379
+ return c.json({ error: `not found: ${file.path}` }, 404);
2380
+ }
2381
+ const currentVersion = fileContentVersion(before);
2382
+ if (currentVersion !== file.expectedVersion) {
2383
+ return c.json(
2384
+ {
2385
+ error: `file conflict: ${file.path}`,
2386
+ path: file.path,
2387
+ currentVersion,
2388
+ currentContent: before
2389
+ },
2390
+ 409
2391
+ );
2392
+ }
2393
+ let folded;
2394
+ try {
2395
+ folded = await foldAtomicCutFile(c, file, absPath, before);
2396
+ } catch (error) {
2397
+ const message = error instanceof Error ? error.message : "Cut transform failed";
2398
+ return c.json({ error: message }, 400);
2399
+ }
2400
+ if (folded instanceof Response) return folded;
2401
+ prepared.push(folded);
2402
+ }
2403
+ for (const file of prepared) {
2404
+ const current = readFileSync5(file.absPath, "utf-8");
2405
+ if (current !== file.before) {
2406
+ return c.json(
2407
+ {
2408
+ error: `file conflict: ${file.path}`,
2409
+ path: file.path,
2410
+ currentVersion: fileContentVersion(current),
2411
+ currentContent: current
2412
+ },
2413
+ 409
2414
+ );
2415
+ }
2416
+ }
2417
+ const backups = /* @__PURE__ */ new Map();
2418
+ for (const file of prepared) {
2419
+ const backup = snapshotBeforeWrite(project.dir, file.absPath);
2420
+ if (backup.error) {
2421
+ return c.json(
2422
+ { error: `Failed to create backup for ${file.path}: ${backup.error}` },
2423
+ 500
2424
+ );
2425
+ }
2426
+ backups.set(file.path, backupPathForResponse(project.dir, backup.backupPath));
2427
+ }
2428
+ const writeToken = createWriteToken(
2429
+ typeof body.transactionToken === "string" ? body.transactionToken : c.req.header("X-Hyperframes-Write-Token")
2430
+ );
2431
+ const written = [];
2432
+ try {
2433
+ for (const file of prepared) {
2434
+ writeFileSync4(file.absPath, file.after, "utf-8");
2435
+ written.push(file);
2436
+ recordFileWriteReceipt(file.absPath, {
2437
+ path: file.path,
2438
+ version: fileContentVersion(file.after),
2439
+ writeToken
2440
+ });
2441
+ }
2442
+ } catch (error) {
2443
+ const conflicts = [];
2444
+ for (const file of written.reverse()) {
2445
+ try {
2446
+ const current = readFileSync5(file.absPath, "utf-8");
2447
+ if (current !== file.after) {
2448
+ conflicts.push(file.path);
2449
+ continue;
2450
+ }
2451
+ writeFileSync4(file.absPath, file.before, "utf-8");
2452
+ recordFileWriteReceipt(file.absPath, {
2453
+ path: file.path,
2454
+ version: fileContentVersion(file.before),
2455
+ writeToken
2456
+ });
2457
+ } catch {
2458
+ conflicts.push(file.path);
2459
+ }
2460
+ }
2461
+ return c.json(
2462
+ {
2463
+ error: error instanceof Error ? error.message : "Cut write failed",
2464
+ outcome: conflicts.length ? "aborted-with-conflicts" : "aborted-restored",
2465
+ conflicts
2466
+ },
2467
+ conflicts.length ? 409 : 500
2468
+ );
2469
+ }
2470
+ const result = prepared.map((file) => ({
2471
+ path: file.path,
2472
+ before: file.before,
2473
+ after: file.after,
2474
+ version: fileContentVersion(file.after),
2475
+ writeToken,
2476
+ backupPath: backups.get(file.path) ?? null,
2477
+ splitCount: file.splitCount,
2478
+ skippedSelectors: file.skippedSelectors
2479
+ }));
2480
+ return c.json({ ok: true, outcome: "committed", files: result });
2481
+ });
2482
+ });
2427
2483
  api.post("/projects/:id/file-mutations/split-element/*", async (c) => {
2428
2484
  const ctx = await resolveFileMutationContext(c, adapter, "split-element");
2429
2485
  if ("error" in ctx) return ctx.error;
@@ -2435,7 +2491,7 @@ function registerFileRoutes(api, adapter) {
2435
2491
  const fallbackTiming = typeof parsed.body.elementStart === "number" && typeof parsed.body.elementDuration === "number" ? { start: parsed.body.elementStart, duration: parsed.body.elementDuration } : void 0;
2436
2492
  let originalContent;
2437
2493
  try {
2438
- originalContent = readFileSync4(ctx.absPath, "utf-8");
2494
+ originalContent = readFileSync5(ctx.absPath, "utf-8");
2439
2495
  } catch {
2440
2496
  return c.json({ error: "not found" }, 404);
2441
2497
  }
@@ -2486,7 +2542,7 @@ function registerFileRoutes(api, adapter) {
2486
2542
  }
2487
2543
  let originalContent;
2488
2544
  try {
2489
- originalContent = readFileSync4(ctx.absPath, "utf-8");
2545
+ originalContent = readFileSync5(ctx.absPath, "utf-8");
2490
2546
  } catch {
2491
2547
  return c.json({ error: "not found" }, 404);
2492
2548
  }
@@ -2576,7 +2632,7 @@ function registerFileRoutes(api, adapter) {
2576
2632
  }
2577
2633
  let originalContent;
2578
2634
  try {
2579
- originalContent = readFileSync4(ctx.absPath, "utf-8");
2635
+ originalContent = readFileSync5(ctx.absPath, "utf-8");
2580
2636
  } catch {
2581
2637
  return c.json({ error: "not found" }, 404);
2582
2638
  }
@@ -2618,7 +2674,7 @@ function registerFileRoutes(api, adapter) {
2618
2674
  if ("error" in parsed) return parsed.error;
2619
2675
  let originalContent;
2620
2676
  try {
2621
- originalContent = readFileSync4(ctx.absPath, "utf-8");
2677
+ originalContent = readFileSync5(ctx.absPath, "utf-8");
2622
2678
  } catch {
2623
2679
  return c.json({ error: "not found" }, 404);
2624
2680
  }
@@ -2647,7 +2703,7 @@ function registerFileRoutes(api, adapter) {
2647
2703
  if ("error" in parsed) return parsed.error;
2648
2704
  let content;
2649
2705
  try {
2650
- content = readFileSync4(ctx.absPath, "utf-8");
2706
+ content = readFileSync5(ctx.absPath, "utf-8");
2651
2707
  } catch {
2652
2708
  return c.json({ exists: false });
2653
2709
  }
@@ -2665,7 +2721,7 @@ function registerFileRoutes(api, adapter) {
2665
2721
  if (!newAbs) {
2666
2722
  return c.json({ error: "forbidden" }, 403);
2667
2723
  }
2668
- if (existsSync3(newAbs)) {
2724
+ if (existsSync4(newAbs)) {
2669
2725
  return c.json({ error: "already exists" }, 409);
2670
2726
  }
2671
2727
  ensureDir(newAbs);
@@ -2681,7 +2737,7 @@ function registerFileRoutes(api, adapter) {
2681
2737
  return c.json({ error: "path required" }, 400);
2682
2738
  }
2683
2739
  const srcAbs = resolveWithinProject(project.dir, body.path);
2684
- if (!srcAbs || !existsSync3(srcAbs)) {
2740
+ if (!srcAbs || !existsSync4(srcAbs)) {
2685
2741
  return c.json({ error: "not found" }, 404);
2686
2742
  }
2687
2743
  const copyPath = generateCopyPath(project.dir, body.path);
@@ -2690,7 +2746,7 @@ function registerFileRoutes(api, adapter) {
2690
2746
  return c.json({ error: "forbidden" }, 403);
2691
2747
  }
2692
2748
  ensureDir(destAbs);
2693
- writeFileSync4(destAbs, readFileSync4(srcAbs));
2749
+ writeFileSync4(destAbs, readFileSync5(srcAbs));
2694
2750
  return c.json({ ok: true, path: copyPath }, 201);
2695
2751
  });
2696
2752
  const MAX_UPLOAD_BYTES = 500 * 1024 * 1024;
@@ -2706,7 +2762,7 @@ function registerFileRoutes(api, adapter) {
2706
2762
  const subDir = c.req.query("dir") ?? "";
2707
2763
  const targetDir = subDir ? resolveWithinProject(project.dir, subDir) : project.dir;
2708
2764
  if (!targetDir) return c.json({ error: "forbidden" }, 403);
2709
- if (subDir && !existsSync3(targetDir)) mkdirSync3(targetDir, { recursive: true });
2765
+ if (subDir && !existsSync4(targetDir)) mkdirSync3(targetDir, { recursive: true });
2710
2766
  const formData = await c.req.formData();
2711
2767
  const result = await processUploadedFiles(formData, targetDir, project.dir);
2712
2768
  return c.json(
@@ -2720,7 +2776,7 @@ function registerFileRoutes(api, adapter) {
2720
2776
  mustExist: true
2721
2777
  });
2722
2778
  if ("error" in res) return res.error;
2723
- const html = readFileSync4(res.absPath, "utf-8");
2779
+ const html = readFileSync5(res.absPath, "utf-8");
2724
2780
  const block = extractGsapScriptBlock(html);
2725
2781
  if (!block) {
2726
2782
  return c.json({
@@ -2779,7 +2835,7 @@ function registerFileRoutes(api, adapter) {
2779
2835
  if (!body || typeof body.expected !== "string" || typeof body.restore !== "string") {
2780
2836
  return c.json({ error: "expected and restore contents required" }, 400);
2781
2837
  }
2782
- const current = readFileSync4(res.absPath, "utf-8");
2838
+ const current = readFileSync5(res.absPath, "utf-8");
2783
2839
  if (current !== body.expected) {
2784
2840
  return c.json({ ok: true, restored: false, conflict: true });
2785
2841
  }
@@ -2789,13 +2845,13 @@ function registerFileRoutes(api, adapter) {
2789
2845
  }
2790
2846
 
2791
2847
  // src/routes/preview.ts
2792
- import { existsSync as existsSync5, readFileSync as readFileSync7, statSync as statSync2 } from "fs";
2848
+ import { existsSync as existsSync6, readFileSync as readFileSync8, statSync as statSync2 } from "fs";
2793
2849
  import { join as join8 } from "path";
2794
2850
  import { createHash as createHash3 } from "crypto";
2795
2851
  import { injectScriptsIntoHtml, stripEmbeddedRuntimeScripts as stripEmbeddedRuntimeScripts2 } from "@hyperframes/core/compiler";
2796
2852
 
2797
2853
  // src/helpers/subComposition.ts
2798
- import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
2854
+ import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
2799
2855
  import { join as join7 } from "path";
2800
2856
  import { parseHTML as parseHTML3 } from "linkedom";
2801
2857
  import {
@@ -2903,8 +2959,8 @@ function tagRootCompositionFile(bodyHtml, compPath) {
2903
2959
  }
2904
2960
  function buildSubCompositionHtml(projectDir, compPath, runtimeUrl, baseHref, rawOverride) {
2905
2961
  const compFile = join7(projectDir, compPath);
2906
- if (!existsSync4(compFile)) return null;
2907
- const rawComp = rawOverride ?? readFileSync5(compFile, "utf-8");
2962
+ if (!existsSync5(compFile)) return null;
2963
+ const rawComp = rawOverride ?? readFileSync6(compFile, "utf-8");
2908
2964
  let compHeadContent = "";
2909
2965
  let rewrittenContent;
2910
2966
  let htmlAttrs = "";
@@ -2936,8 +2992,8 @@ function buildSubCompositionHtml(projectDir, compPath, runtimeUrl, baseHref, raw
2936
2992
  rewrittenContent = tagRootCompositionFile(rewrittenContent, compPath);
2937
2993
  const indexPath = join7(projectDir, "index.html");
2938
2994
  let headContent = "";
2939
- if (existsSync4(indexPath)) {
2940
- const indexHtml = readFileSync5(indexPath, "utf-8");
2995
+ if (existsSync5(indexPath)) {
2996
+ const indexHtml = readFileSync6(indexPath, "utf-8");
2941
2997
  const headMatch = indexHtml.match(/<head[^>]*>([\s\S]*?)<\/head>/i);
2942
2998
  headContent = headMatch?.[1] ?? "";
2943
2999
  }
@@ -2970,215 +3026,28 @@ ${rewrittenContent}
2970
3026
  </html>`;
2971
3027
  }
2972
3028
 
2973
- // src/helpers/studioMotionRenderScript.ts
2974
- var STUDIO_MOTION_PATH = ".hyperframes/studio-motion.json";
2975
- function hasStudioMotionEntries(manifestContent) {
2976
- try {
2977
- const parsed = JSON.parse(manifestContent);
2978
- return Array.isArray(parsed.motions) && parsed.motions.length > 0;
2979
- } catch {
2980
- return false;
2981
- }
2982
- }
2983
- function createStudioMotionRenderBodyScript(manifestContent, options = {}) {
2984
- if (!manifestContent.trim() || !hasStudioMotionEntries(manifestContent)) return null;
2985
- return `(${studioMotionRenderRuntime.toString()})(${JSON.stringify(manifestContent)}, ${JSON.stringify(options.activeCompositionPath ?? null)});`;
2986
- }
2987
- function studioMotionRenderRuntime(manifestContent, activeCompositionPath) {
2988
- const STUDIO_MOTION_TIMELINE_ID = "studio-motion";
2989
- const STUDIO_MOTION_ATTR = "data-hf-studio-motion";
2990
- const ORIGINAL_TRANSFORM_ATTR = "data-hf-studio-motion-original-transform";
2991
- const ORIGINAL_OPACITY_ATTR = "data-hf-studio-motion-original-opacity";
2992
- const ORIGINAL_VISIBILITY_ATTR = "data-hf-studio-motion-original-visibility";
2993
- const objectRecord = (value) => value && typeof value === "object" ? value : null;
2994
- const finiteNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
2995
- const runtimeWindow = window;
2996
- const parseMotionValues = (value) => {
2997
- const record = objectRecord(value);
2998
- if (!record) return null;
2999
- const parsed = {};
3000
- for (const key of ["x", "y", "scale", "rotation", "opacity", "autoAlpha"]) {
3001
- const next = finiteNumber(record[key]);
3002
- if (next != null) parsed[key] = next;
3003
- }
3004
- return Object.keys(parsed).length > 0 ? parsed : null;
3005
- };
3006
- const parseCustomEase = (value) => {
3007
- const record = objectRecord(value);
3008
- if (!record) return null;
3009
- const id = typeof record.id === "string" ? record.id.trim() : "";
3010
- const data = typeof record.data === "string" ? record.data.trim() : "";
3011
- if (!id || !data) return null;
3012
- return { id, data };
3013
- };
3014
- const parsedManifest = (() => {
3015
- try {
3016
- return objectRecord(JSON.parse(manifestContent));
3017
- } catch {
3018
- return null;
3019
- }
3020
- })();
3021
- const manifestMotions = Array.isArray(parsedManifest?.motions) ? parsedManifest.motions : [];
3022
- const sourceFileForElement = (element) => {
3023
- let current = element;
3024
- while (current) {
3025
- const sourceFile = current.getAttribute("data-composition-file") ?? current.getAttribute("data-composition-src");
3026
- if (sourceFile) return sourceFile;
3027
- current = current.parentElement;
3028
- }
3029
- return activeCompositionPath ?? "index.html";
3030
- };
3031
- const elementMatchesSourceFile = (element, sourceFile) => sourceFileForElement(element) === sourceFile;
3032
- const isHTMLElement2 = (element) => element instanceof HTMLElement;
3033
- const querySelectorCandidates = (selector) => {
3034
- const className = selector.match(/^\.([A-Za-z0-9_-]+)$/)?.[1];
3035
- if (className) {
3036
- return Array.from(document.getElementsByTagName("*")).filter(
3037
- (element) => isHTMLElement2(element) && element.classList.contains(className)
3038
- );
3039
- }
3040
- if (/^[A-Za-z][A-Za-z0-9-]*$/.test(selector)) {
3041
- return Array.from(document.getElementsByTagName(selector)).filter(isHTMLElement2);
3042
- }
3043
- return Array.from(document.querySelectorAll(selector)).filter(isHTMLElement2);
3044
- };
3045
- const resolveTarget = (targetRecord) => {
3046
- const sourceFile = typeof targetRecord.sourceFile === "string" ? targetRecord.sourceFile : "";
3047
- if (!sourceFile) return null;
3048
- const id = typeof targetRecord.id === "string" ? targetRecord.id : "";
3049
- if (id) {
3050
- const byId = document.getElementById(id);
3051
- if (isHTMLElement2(byId) && elementMatchesSourceFile(byId, sourceFile)) return byId;
3052
- }
3053
- const selector = typeof targetRecord.selector === "string" ? targetRecord.selector : "";
3054
- if (!selector) return null;
3055
- try {
3056
- const selectorIndex = Math.max(0, Math.floor(finiteNumber(targetRecord.selectorIndex) ?? 0));
3057
- return querySelectorCandidates(selector).filter(
3058
- (element) => elementMatchesSourceFile(element, sourceFile)
3059
- )[selectorIndex] ?? null;
3060
- } catch {
3061
- return null;
3062
- }
3063
- };
3064
- const restoreElement = (element) => {
3065
- runtimeWindow.gsap?.set?.(element, { clearProps: "transform,opacity,visibility" });
3066
- element.style.transform = element.getAttribute(ORIGINAL_TRANSFORM_ATTR) ?? "";
3067
- element.style.opacity = element.getAttribute(ORIGINAL_OPACITY_ATTR) ?? "";
3068
- element.style.visibility = element.getAttribute(ORIGINAL_VISIBILITY_ATTR) ?? "";
3069
- element.removeAttribute(STUDIO_MOTION_ATTR);
3070
- element.removeAttribute(ORIGINAL_TRANSFORM_ATTR);
3071
- element.removeAttribute(ORIGINAL_OPACITY_ATTR);
3072
- element.removeAttribute(ORIGINAL_VISIBILITY_ATTR);
3073
- };
3074
- const restoreStudioMotionElements = () => {
3075
- for (const element of Array.from(document.querySelectorAll(`[${STUDIO_MOTION_ATTR}]`))) {
3076
- if (isHTMLElement2(element)) restoreElement(element);
3077
- }
3078
- };
3079
- const readCurrentTime = () => {
3080
- try {
3081
- const playerTime = runtimeWindow.__player?.getTime?.();
3082
- if (typeof playerTime === "number" && Number.isFinite(playerTime)) {
3083
- return Math.max(0, playerTime);
3084
- }
3085
- } catch {
3086
- }
3087
- try {
3088
- const timelineTime = runtimeWindow.__timeline?.time?.();
3089
- if (typeof timelineTime === "number" && Number.isFinite(timelineTime)) {
3090
- return Math.max(0, timelineTime);
3091
- }
3092
- } catch {
3093
- }
3094
- return 0;
3095
- };
3096
- const resolveEase = (motion) => {
3097
- const fallback = typeof motion.ease === "string" && motion.ease.trim() ? motion.ease.trim() : "none";
3098
- const customEase = parseCustomEase(motion.customEase);
3099
- const customEasePlugin = runtimeWindow.CustomEase;
3100
- if (!customEase || typeof customEasePlugin?.create !== "function") return fallback;
3101
- try {
3102
- runtimeWindow.gsap?.registerPlugin?.(customEasePlugin);
3103
- customEasePlugin.create(customEase.id, customEase.data);
3104
- return customEase.id;
3105
- } catch {
3106
- return fallback;
3107
- }
3108
- };
3109
- const applyManifest = () => {
3110
- runtimeWindow.__timelines = runtimeWindow.__timelines ?? {};
3111
- runtimeWindow.__timelines[STUDIO_MOTION_TIMELINE_ID]?.kill?.();
3112
- delete runtimeWindow.__timelines[STUDIO_MOTION_TIMELINE_ID];
3113
- restoreStudioMotionElements();
3114
- const gsap = runtimeWindow.gsap;
3115
- if (!gsap?.timeline || manifestMotions.length === 0) return 0;
3116
- const timeline = gsap.timeline({ paused: true, defaults: { overwrite: "auto" } });
3117
- let applied = 0;
3118
- for (const motionValue of manifestMotions) {
3119
- const motion = objectRecord(motionValue);
3120
- if (!motion || motion.kind !== "gsap-motion") continue;
3121
- const targetRecord = objectRecord(motion.target);
3122
- if (!targetRecord) continue;
3123
- const target = resolveTarget(targetRecord);
3124
- if (!target || typeof timeline.fromTo !== "function") continue;
3125
- const start = finiteNumber(motion.start);
3126
- const duration = finiteNumber(motion.duration);
3127
- if (start == null || duration == null || start < 0 || duration <= 0) continue;
3128
- const from = parseMotionValues(motion.from);
3129
- const to = parseMotionValues(motion.to);
3130
- if (!from || !to) continue;
3131
- if (!target.hasAttribute(STUDIO_MOTION_ATTR)) {
3132
- target.setAttribute(ORIGINAL_TRANSFORM_ATTR, target.style.transform);
3133
- target.setAttribute(ORIGINAL_OPACITY_ATTR, target.style.opacity);
3134
- target.setAttribute(ORIGINAL_VISIBILITY_ATTR, target.style.visibility);
3135
- }
3136
- target.setAttribute(STUDIO_MOTION_ATTR, "true");
3137
- timeline.fromTo(
3138
- target,
3139
- from,
3140
- { ...to, duration, ease: resolveEase(motion), overwrite: "auto", immediateRender: false },
3141
- start
3142
- );
3143
- applied += 1;
3144
- }
3145
- if (applied === 0) {
3146
- timeline.kill?.();
3147
- return 0;
3148
- }
3149
- runtimeWindow.__timelines[STUDIO_MOTION_TIMELINE_ID] = timeline;
3150
- timeline.pause?.();
3151
- const currentTime = readCurrentTime();
3152
- if (typeof timeline.totalTime === "function") timeline.totalTime(currentTime, false);
3153
- else timeline.time?.(currentTime);
3154
- return applied;
3155
- };
3156
- runtimeWindow.__hfStudioMotionApply = applyManifest;
3157
- applyManifest();
3158
- }
3159
-
3160
3029
  // src/routes/preview.ts
3161
- import { ensureHfIds as ensureHfIds3 } from "@hyperframes/parsers/hf-ids";
3030
+ import { ensureHfIds as ensureHfIds2 } from "@hyperframes/parsers/hf-ids";
3162
3031
 
3163
3032
  // src/helpers/hfIdPersist.ts
3164
- import { ensureHfIds as ensureHfIds2 } from "@hyperframes/parsers/hf-ids";
3033
+ import { ensureHfIds } from "@hyperframes/parsers/hf-ids";
3165
3034
  import {
3166
3035
  closeSync as closeSync2,
3167
3036
  constants,
3168
3037
  fstatSync,
3169
3038
  ftruncateSync as ftruncateSync2,
3170
3039
  openSync as openSync2,
3171
- readFileSync as readFileSync6,
3040
+ readFileSync as readFileSync7,
3172
3041
  writeFileSync as writeFileSync5,
3173
3042
  writeSync as writeSync2
3174
3043
  } from "fs";
3175
3044
  function persistHfIdsIfNeeded(filePath, html) {
3176
- const normalized = ensureHfIds2(html);
3045
+ const normalized = ensureHfIds(html);
3177
3046
  const idsBefore = (html.match(/\bdata-hf-id=/g) ?? []).length;
3178
3047
  const idsAfter = (normalized.match(/\bdata-hf-id=/g) ?? []).length;
3179
3048
  if (idsAfter > idsBefore) {
3180
3049
  try {
3181
- const current = readFileSync6(filePath, "utf-8");
3050
+ const current = readFileSync7(filePath, "utf-8");
3182
3051
  if (current === html) {
3183
3052
  writeFileSync5(filePath, normalized, "utf-8");
3184
3053
  }
@@ -3206,8 +3075,8 @@ function stampFileHfIds(filePath) {
3206
3075
  if (fd === null) return null;
3207
3076
  try {
3208
3077
  if (!fstatSync(fd).isFile()) return null;
3209
- const html = readFileSync6(fd, "utf-8");
3210
- const normalized = ensureHfIds2(html);
3078
+ const html = readFileSync7(fd, "utf-8");
3079
+ const normalized = ensureHfIds(html);
3211
3080
  const idsBefore = (html.match(/\bdata-hf-id=/g) ?? []).length;
3212
3081
  const idsAfter = (normalized.match(/\bdata-hf-id=/g) ?? []).length;
3213
3082
  if (writable && idsAfter > idsBefore) {
@@ -3250,9 +3119,9 @@ ${html}`;
3250
3119
  }
3251
3120
  function readStudioMotionManifestContent(projectDir) {
3252
3121
  const manifestPath = join8(projectDir, STUDIO_MOTION_PATH);
3253
- if (!existsSync5(manifestPath)) return "";
3122
+ if (!existsSync6(manifestPath)) return "";
3254
3123
  try {
3255
- return readFileSync7(manifestPath, "utf-8");
3124
+ return readFileSync8(manifestPath, "utf-8");
3256
3125
  } catch {
3257
3126
  return "";
3258
3127
  }
@@ -3408,12 +3277,18 @@ async function transformPreviewHtml(html, adapter, project, activeCompositionPat
3408
3277
  }
3409
3278
  function resolveProjectMainHtml(projectDir, projectId) {
3410
3279
  const indexPath = join8(projectDir, "index.html");
3411
- if (existsSync5(indexPath)) {
3412
- return { html: readFileSync7(indexPath, "utf-8"), compositionPath: "index.html" };
3280
+ if (existsSync6(indexPath)) {
3281
+ return {
3282
+ html: readFileSync8(indexPath, "utf-8"),
3283
+ compositionPath: "index.html"
3284
+ };
3413
3285
  }
3414
3286
  const blockHtmlPath = join8(projectDir, `${projectId}.html`);
3415
- if (existsSync5(blockHtmlPath)) {
3416
- return { html: readFileSync7(blockHtmlPath, "utf-8"), compositionPath: `${projectId}.html` };
3287
+ if (existsSync6(blockHtmlPath)) {
3288
+ return {
3289
+ html: readFileSync8(blockHtmlPath, "utf-8"),
3290
+ compositionPath: `${projectId}.html`
3291
+ };
3417
3292
  }
3418
3293
  return null;
3419
3294
  }
@@ -3422,6 +3297,7 @@ function registerPreviewRoutes(api, adapter) {
3422
3297
  "Cache-Control": "private, no-cache",
3423
3298
  ETag: etag
3424
3299
  });
3300
+ const mediaCodecProbeCache = resolvePreviewMediaCodecProbeCache(adapter);
3425
3301
  api.get("/projects/:id/preview", async (c) => {
3426
3302
  const resolved = await resolveProjectAndSignature(adapter, c.req.param("id"));
3427
3303
  if (!resolved) return c.json({ error: "not found" }, 404);
@@ -3432,7 +3308,10 @@ function registerPreviewRoutes(api, adapter) {
3432
3308
  const etag = `"preview:${signature}${variablesEtagSalt(vars.raw)}"`;
3433
3309
  const ifNoneMatch = c.req.header("If-None-Match");
3434
3310
  if (ifNoneMatch === etag) {
3435
- return new Response(null, { status: 304, headers: previewCacheHeaders(etag) });
3311
+ return new Response(null, {
3312
+ status: 304,
3313
+ headers: previewCacheHeaders(etag)
3314
+ });
3436
3315
  }
3437
3316
  const diskMain = resolveProjectMainHtml(project.dir, project.id);
3438
3317
  const normalizedDisk = diskMain ? persistHfIdsIfNeeded(join8(project.dir, diskMain.compositionPath), diskMain.html) : null;
@@ -3455,12 +3334,19 @@ ${runtimeTag}`;
3455
3334
  bundled = bundled.replace(/<head>/i, `<head><base href="${baseHref}">`);
3456
3335
  }
3457
3336
  bundled = injectStudioPreviewAugmentations(
3458
- ensureHfIds3(await transformPreviewHtml(bundled, adapter, project, mainCompositionPath)),
3337
+ ensureHfIds2(await transformPreviewHtml(bundled, adapter, project, mainCompositionPath)),
3459
3338
  adapter,
3460
3339
  project.dir,
3461
3340
  mainCompositionPath
3462
3341
  );
3463
3342
  if (previewVariables) bundled = injectPreviewVariables(bundled, previewVariables);
3343
+ bundled = await injectMediaCodecMap(
3344
+ bundled,
3345
+ adapter,
3346
+ project.dir,
3347
+ mainCompositionPath,
3348
+ mediaCodecProbeCache
3349
+ );
3464
3350
  return c.html(bundled, 200, previewCacheHeaders(etag));
3465
3351
  } catch {
3466
3352
  const fallback = resolveProjectMainHtml(project.dir, project.id);
@@ -3478,6 +3364,13 @@ ${runtimeTag}`;
3478
3364
  if (previewVariables) {
3479
3365
  fallbackAugmented = injectPreviewVariables(fallbackAugmented, previewVariables);
3480
3366
  }
3367
+ fallbackAugmented = await injectMediaCodecMap(
3368
+ fallbackAugmented,
3369
+ adapter,
3370
+ project.dir,
3371
+ fallback.compositionPath,
3372
+ mediaCodecProbeCache
3373
+ );
3481
3374
  return c.html(fallbackAugmented, 200, previewCacheHeaders(etag));
3482
3375
  }
3483
3376
  return c.text("not found", 404);
@@ -3498,13 +3391,16 @@ ${runtimeTag}`;
3498
3391
  c.req.path.replace(`/projects/${project.id}/preview/comp/`, "").split("?")[0] ?? ""
3499
3392
  );
3500
3393
  const compFile = resolveWithinProject(project.dir, compPath);
3501
- if (!compFile || !existsSync5(compFile) || !statSync2(compFile).isFile()) {
3394
+ if (!compFile || !existsSync6(compFile) || !statSync2(compFile).isFile()) {
3502
3395
  return c.text("not found", 404);
3503
3396
  }
3504
3397
  const etag = `"comp:v2:${compPath}:${signature}${variablesEtagSalt(vars.raw)}"`;
3505
3398
  const ifNoneMatch = c.req.header("If-None-Match");
3506
3399
  if (ifNoneMatch === etag) {
3507
- return new Response(null, { status: 304, headers: previewCacheHeaders(etag) });
3400
+ return new Response(null, {
3401
+ status: 304,
3402
+ headers: previewCacheHeaders(etag)
3403
+ });
3508
3404
  }
3509
3405
  const stamped = pinSubCompHfIds(compFile, compPath);
3510
3406
  if (stamped === null) return c.text("not found", 404);
@@ -3517,9 +3413,10 @@ ${runtimeTag}`;
3517
3413
  stamped
3518
3414
  );
3519
3415
  if (!html) return c.text("not found", 404);
3520
- html = ensureHfIds3(await transformPreviewHtml(html, adapter, project, compPath));
3416
+ html = ensureHfIds2(await transformPreviewHtml(html, adapter, project, compPath));
3521
3417
  html = injectStudioPreviewAugmentations(html, adapter, project.dir, compPath);
3522
3418
  if (previewVariables) html = injectPreviewVariables(html, previewVariables);
3419
+ html = await injectMediaCodecMap(html, adapter, project.dir, compPath, mediaCodecProbeCache);
3523
3420
  return c.html(html, 200, previewCacheHeaders(etag));
3524
3421
  });
3525
3422
  api.get("/projects/:id/preview/*", async (c) => {
@@ -3532,21 +3429,55 @@ ${runtimeTag}`;
3532
3429
  if (!file) {
3533
3430
  return c.text("not found", 404);
3534
3431
  }
3535
- const stat = existsSync5(file) ? statSync2(file) : null;
3432
+ const stat = existsSync6(file) ? statSync2(file) : null;
3536
3433
  if (!stat?.isFile()) {
3537
3434
  return c.text("not found", 404);
3538
3435
  }
3539
3436
  const contentType = getMimeType(subPath);
3540
3437
  const isText = /\.(html|css|js|json|svg|txt|md|cube)$/i.test(subPath);
3541
- const etag = `"${stat.mtimeMs.toString(36)}-${stat.size.toString(36)}"`;
3542
- const cacheHeaders = isText ? { "Cache-Control": "no-store" } : { "Cache-Control": "private, max-age=3600, must-revalidate", ETag: etag };
3438
+ const proxyParam = c.req.query("hf-proxy");
3439
+ let proxyVariant;
3440
+ if (proxyParam !== void 0) {
3441
+ if (!isProxyVariantRequest(proxyParam) || !contentType.startsWith("video/") || !isAutoProxyEnabled(adapter)) {
3442
+ return c.text("not found", 404);
3443
+ }
3444
+ const facts = await probeAssetCodec(file);
3445
+ const eligibility = decideMediaProxyEligibility(facts);
3446
+ if (!eligibility.eligible) {
3447
+ return c.text(`media proxy unavailable: ${eligibility.reason}`, 422);
3448
+ }
3449
+ if (!facts) return c.text("media proxy unavailable: unknown_codec", 422);
3450
+ proxyVariant = resolveProxyVariantRequest(proxyParam, facts) ?? void 0;
3451
+ if (!proxyVariant) {
3452
+ return c.text("media proxy variant does not match asset", 422);
3453
+ }
3454
+ }
3455
+ const etag = `"${stat.mtimeMs.toString(36)}-${stat.size.toString(36)}${proxyEtagSalt(proxyVariant)}"`;
3456
+ const cacheHeaders = isText ? { "Cache-Control": "no-store" } : {
3457
+ "Cache-Control": "private, max-age=3600, must-revalidate",
3458
+ ETag: etag
3459
+ };
3543
3460
  if (!isText) {
3544
3461
  const ifNoneMatch = c.req.header("If-None-Match");
3545
3462
  if (ifNoneMatch === etag) {
3546
3463
  return new Response(null, { status: 304, headers: cacheHeaders });
3547
3464
  }
3548
3465
  }
3549
- const buffer = isText ? Buffer.from(readFileSync7(file, "utf-8"), "utf-8") : readFileSync7(file);
3466
+ let servedPath = file;
3467
+ let servedContentType = contentType;
3468
+ if (proxyVariant !== void 0) {
3469
+ try {
3470
+ servedPath = await resolveProxy(project.dir, file, proxyVariant);
3471
+ } catch (err) {
3472
+ if (err instanceof ProxyCapacityError) {
3473
+ return c.text(err.message, 503, { "Retry-After": "5" });
3474
+ }
3475
+ const message = err instanceof ProxyTranscodeError ? err.message : "proxy transcode failed";
3476
+ return c.text(message, 502);
3477
+ }
3478
+ servedContentType = PROXY_VARIANT_CONFIG[proxyVariant].contentType;
3479
+ }
3480
+ const buffer = isText ? Buffer.from(readFileSync8(file, "utf-8"), "utf-8") : readFileSync8(servedPath);
3550
3481
  const totalSize = buffer.length;
3551
3482
  const rangeHeader = c.req.header("Range");
3552
3483
  if (rangeHeader) {
@@ -3560,7 +3491,7 @@ ${runtimeTag}`;
3560
3491
  status: 206,
3561
3492
  headers: {
3562
3493
  ...cacheHeaders,
3563
- "Content-Type": contentType,
3494
+ "Content-Type": servedContentType,
3564
3495
  "Content-Range": `bytes ${start}-${safeEnd}/${totalSize}`,
3565
3496
  "Accept-Ranges": "bytes",
3566
3497
  "Content-Length": String(chunkSize)
@@ -3571,7 +3502,7 @@ ${runtimeTag}`;
3571
3502
  return new Response(new Uint8Array(buffer), {
3572
3503
  headers: {
3573
3504
  ...cacheHeaders,
3574
- "Content-Type": contentType,
3505
+ "Content-Type": servedContentType,
3575
3506
  "Accept-Ranges": "bytes",
3576
3507
  "Content-Length": String(totalSize)
3577
3508
  }
@@ -3580,7 +3511,7 @@ ${runtimeTag}`;
3580
3511
  }
3581
3512
 
3582
3513
  // src/routes/lint.ts
3583
- import { readFileSync as readFileSync8 } from "fs";
3514
+ import { readFileSync as readFileSync9 } from "fs";
3584
3515
  import { join as join9 } from "path";
3585
3516
  function registerLintRoutes(api, adapter) {
3586
3517
  api.get("/projects/:id/lint", async (c) => {
@@ -3592,7 +3523,7 @@ function registerLintRoutes(api, adapter) {
3592
3523
  );
3593
3524
  const allFindings = [];
3594
3525
  for (const file of htmlFiles) {
3595
- const content = readFileSync8(join9(project.dir, file), "utf-8");
3526
+ const content = readFileSync9(join9(project.dir, file), "utf-8");
3596
3527
  const result = await adapter.lint(content, { filePath: file });
3597
3528
  if (result?.findings) {
3598
3529
  for (const f of result.findings) {
@@ -3610,7 +3541,7 @@ function registerLintRoutes(api, adapter) {
3610
3541
 
3611
3542
  // src/routes/render.ts
3612
3543
  import { streamSSE } from "hono/streaming";
3613
- import { existsSync as existsSync6, readFileSync as readFileSync9, mkdirSync as mkdirSync4, unlinkSync as unlinkSync3, readdirSync as readdirSync5, statSync as statSync3 } from "fs";
3544
+ import { existsSync as existsSync7, readFileSync as readFileSync10, mkdirSync as mkdirSync4, unlinkSync as unlinkSync3, readdirSync as readdirSync5, statSync as statSync3 } from "fs";
3614
3545
  import { join as join10 } from "path";
3615
3546
  import { VALID_CANVAS_RESOLUTIONS } from "@hyperframes/parsers";
3616
3547
  import { formatRenderOutputTimestamp, parseFps } from "@hyperframes/core";
@@ -3669,7 +3600,7 @@ function registerRenderRoutes(api, adapter) {
3669
3600
  const now = /* @__PURE__ */ new Date();
3670
3601
  const jobId = `${project.id}_${formatRenderOutputTimestamp(now)}`;
3671
3602
  const rendersDir = adapter.rendersDir(project);
3672
- if (!existsSync6(rendersDir)) mkdirSync4(rendersDir, { recursive: true });
3603
+ if (!existsSync7(rendersDir)) mkdirSync4(rendersDir, { recursive: true });
3673
3604
  const ext = FORMAT_EXT[format] ?? ".mp4";
3674
3605
  const outputPath = join10(rendersDir, `${jobId}${ext}`);
3675
3606
  const jobState = adapter.startRender({
@@ -3734,12 +3665,12 @@ function registerRenderRoutes(api, adapter) {
3734
3665
  api.get("/render/:jobId/view", (c) => {
3735
3666
  const { jobId } = c.req.param();
3736
3667
  const job = renderJobs.get(jobId);
3737
- if (!job?.outputPath || !existsSync6(job.outputPath)) {
3668
+ if (!job?.outputPath || !existsSync7(job.outputPath)) {
3738
3669
  return c.json({ error: "not found" }, 404);
3739
3670
  }
3740
3671
  const contentType = renderContentType(job.outputPath);
3741
3672
  const filename = job.outputPath.split("/").pop() ?? `render.mp4`;
3742
- const content = readFileSync9(job.outputPath);
3673
+ const content = readFileSync10(job.outputPath);
3743
3674
  return new Response(content, {
3744
3675
  headers: {
3745
3676
  "Content-Type": contentType,
@@ -3752,12 +3683,12 @@ function registerRenderRoutes(api, adapter) {
3752
3683
  api.get("/render/:jobId/download", (c) => {
3753
3684
  const { jobId } = c.req.param();
3754
3685
  const job = renderJobs.get(jobId);
3755
- if (!job?.outputPath || !existsSync6(job.outputPath)) {
3686
+ if (!job?.outputPath || !existsSync7(job.outputPath)) {
3756
3687
  return c.json({ error: "not found" }, 404);
3757
3688
  }
3758
3689
  const contentType = renderContentType(job.outputPath);
3759
3690
  const filename = job.outputPath.split("/").pop() ?? `render.mp4`;
3760
- const content = readFileSync9(job.outputPath);
3691
+ const content = readFileSync10(job.outputPath);
3761
3692
  return new Response(content, {
3762
3693
  headers: {
3763
3694
  "Content-Type": contentType,
@@ -3772,7 +3703,7 @@ function registerRenderRoutes(api, adapter) {
3772
3703
  const dir = state.outputPath.replace(/\/[^/]+$/, "");
3773
3704
  for (const ext of [".mp4", ".webm", ".mov", ".meta.json"]) {
3774
3705
  const fp = join10(dir, `${jobId}${ext}`);
3775
- if (existsSync6(fp)) unlinkSync3(fp);
3706
+ if (existsSync7(fp)) unlinkSync3(fp);
3776
3707
  }
3777
3708
  break;
3778
3709
  }
@@ -3788,9 +3719,9 @@ function registerRenderRoutes(api, adapter) {
3788
3719
  const rendersDir = adapter.rendersDir(project);
3789
3720
  const fp = resolveWithinProject(rendersDir, filename);
3790
3721
  if (!fp) return c.json({ error: "forbidden" }, 403);
3791
- if (!existsSync6(fp)) return c.json({ error: "not found" }, 404);
3722
+ if (!existsSync7(fp)) return c.json({ error: "not found" }, 404);
3792
3723
  const contentType = renderContentType(fp);
3793
- const content = readFileSync9(fp);
3724
+ const content = readFileSync10(fp);
3794
3725
  return new Response(content, {
3795
3726
  headers: {
3796
3727
  "Content-Type": contentType,
@@ -3804,7 +3735,7 @@ function registerRenderRoutes(api, adapter) {
3804
3735
  const project = await adapter.resolveProject(c.req.param("id"));
3805
3736
  if (!project) return c.json({ error: "not found" }, 404);
3806
3737
  const rendersDir = adapter.rendersDir(project);
3807
- if (!existsSync6(rendersDir)) return c.json({ renders: [] });
3738
+ if (!existsSync7(rendersDir)) return c.json({ renders: [] });
3808
3739
  const files = readdirSync5(rendersDir).filter((f) => f.endsWith(".mp4") || f.endsWith(".webm") || f.endsWith(".mov")).map((f) => {
3809
3740
  const fp = join10(rendersDir, f);
3810
3741
  const stat = statSync3(fp);
@@ -3812,10 +3743,10 @@ function registerRenderRoutes(api, adapter) {
3812
3743
  const metaPath = join10(rendersDir, `${rid}.meta.json`);
3813
3744
  let status = "complete";
3814
3745
  let durationMs;
3815
- if (existsSync6(metaPath)) {
3746
+ if (existsSync7(metaPath)) {
3816
3747
  try {
3817
- const meta = JSON.parse(readFileSync9(metaPath, "utf-8"));
3818
- if (meta.status === "failed") status = "failed";
3748
+ const meta = JSON.parse(readFileSync10(metaPath, "utf-8"));
3749
+ if (meta.status === "failed" && !existsSync7(fp)) status = "failed";
3819
3750
  if (meta.durationMs) durationMs = meta.durationMs;
3820
3751
  } catch {
3821
3752
  }
@@ -3845,589 +3776,9 @@ function registerRenderRoutes(api, adapter) {
3845
3776
  }
3846
3777
 
3847
3778
  // src/routes/thumbnail.ts
3848
- import { existsSync as existsSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, statSync as statSync4 } from "fs";
3779
+ import { existsSync as existsSync8, readFileSync as readFileSync11, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, statSync as statSync4 } from "fs";
3849
3780
  import { join as join11 } from "path";
3850
3781
  import { createHash as createHash4 } from "crypto";
3851
-
3852
- // src/helpers/manualEditsRenderScript.ts
3853
- var STUDIO_MANUAL_EDITS_PATH = ".hyperframes/studio-manual-edits.json";
3854
- function createStudioManualEditsRenderBodyScript(manifestContent, options = {}) {
3855
- if (!manifestContent.trim()) return null;
3856
- return `(${studioManualEditsRenderRuntime.toString()})(${JSON.stringify(manifestContent)}, ${JSON.stringify(options.activeCompositionPath ?? null)});`;
3857
- }
3858
- function createStudioPositionSeekReapplyScript() {
3859
- return `(${studioPositionSeekReapplyRuntime.toString()})();`;
3860
- }
3861
- function studioPositionSeekReapplyRuntime() {
3862
- const OFFSET_X_PROP = "--hf-studio-offset-x";
3863
- const OFFSET_Y_PROP = "--hf-studio-offset-y";
3864
- const WIDTH_PROP = "--hf-studio-width";
3865
- const HEIGHT_PROP = "--hf-studio-height";
3866
- const ROTATION_PROP = "--hf-studio-rotation";
3867
- const PATH_OFFSET_ATTR = "data-hf-studio-path-offset";
3868
- const BOX_SIZE_ATTR = "data-hf-studio-box-size";
3869
- const ROTATION_ATTR = "data-hf-studio-rotation";
3870
- const ORIGINAL_TRANSLATE_ATTR = "data-hf-studio-original-translate";
3871
- const ORIGINAL_ROTATE_ATTR = "data-hf-studio-original-rotate";
3872
- const MOTION_ATTR = "data-hf-studio-motion";
3873
- const MOTION_TL_KEY = "studio-motion";
3874
- const WRAPPED_PROP = "__hfStudioPositionSeekReapplyWrapped";
3875
- if (!document.querySelector("[" + PATH_OFFSET_ATTR + '="true"]') && !document.querySelector("[" + BOX_SIZE_ATTR + '="true"]') && !document.querySelector("[" + ROTATION_ATTR + '="true"]') && !document.querySelector("[" + MOTION_ATTR + "]"))
3876
- return;
3877
- const splitTopLevelWhitespace = (value) => {
3878
- const parts = [];
3879
- let depth = 0;
3880
- let current = "";
3881
- for (const char of value.trim()) {
3882
- if (char === "(") depth += 1;
3883
- if (char === ")") depth = Math.max(0, depth - 1);
3884
- if (/\s/.test(char) && depth === 0) {
3885
- if (current) parts.push(current);
3886
- current = "";
3887
- } else {
3888
- current += char;
3889
- }
3890
- }
3891
- if (current) parts.push(current);
3892
- return parts;
3893
- };
3894
- const composeTranslate = (element, x, y) => {
3895
- const original = element.getAttribute(ORIGINAL_TRANSLATE_ATTR)?.trim();
3896
- if (!original || original === "none") return x + " " + y;
3897
- const parts = splitTopLevelWhitespace(original);
3898
- if (parts.length === 1) return "calc(" + parts[0] + " + " + x + ") " + y;
3899
- if (parts.length >= 2) {
3900
- const z = parts.length >= 3 ? " " + parts[2] : "";
3901
- return "calc(" + parts[0] + " + " + x + ") calc(" + parts[1] + " + " + y + ")" + z;
3902
- }
3903
- return x + " " + y;
3904
- };
3905
- const isSimpleRotateAngle = (value) => /^-?(?:\d+(?:\.\d+)?|\.\d+)(?:deg|rad|turn|grad)$/.test(value.trim());
3906
- const composeRotation = (element, rotationValue) => {
3907
- const original = element.getAttribute(ORIGINAL_ROTATE_ATTR)?.trim();
3908
- if (!original || original === "none" || !isSimpleRotateAngle(original)) return rotationValue;
3909
- return "calc(" + original + " + " + rotationValue + ")";
3910
- };
3911
- let lastSeekTime = 0;
3912
- let cachedMotionKey = "";
3913
- const finiteNum = (v) => typeof v === "number" && Number.isFinite(v) ? v : null;
3914
- const computeMotionKey = (motionEls) => {
3915
- let key = "";
3916
- for (let i = 0; i < motionEls.length; i++) {
3917
- const json = motionEls[i].getAttribute?.(MOTION_ATTR);
3918
- if (json) key += (key ? "\n" : "") + json;
3919
- }
3920
- return key;
3921
- };
3922
- const reapplyMotionTimeline = () => {
3923
- const motionEls = document.querySelectorAll("[" + MOTION_ATTR + "]");
3924
- if (motionEls.length === 0) {
3925
- cachedMotionKey = "";
3926
- return;
3927
- }
3928
- const win = window;
3929
- const gsap = win.gsap;
3930
- if (!gsap || typeof gsap.timeline !== "function") return;
3931
- win.__timelines = win.__timelines || {};
3932
- const motionKey = computeMotionKey(motionEls);
3933
- const existing = win.__timelines[MOTION_TL_KEY];
3934
- if (motionKey && motionKey === cachedMotionKey && existing && typeof existing.totalTime === "function") {
3935
- existing.totalTime(lastSeekTime, false);
3936
- return;
3937
- }
3938
- if (existing && typeof existing.kill === "function") existing.kill();
3939
- const tl = gsap.timeline({ paused: true, defaults: { overwrite: "auto" } });
3940
- const fromTo = tl.fromTo;
3941
- if (typeof fromTo !== "function") return;
3942
- let applied = 0;
3943
- for (let i = 0; i < motionEls.length; i++) {
3944
- const el = motionEls[i];
3945
- if (!(el instanceof HTMLElement)) continue;
3946
- const json = el.getAttribute(MOTION_ATTR);
3947
- if (!json) continue;
3948
- try {
3949
- const m = JSON.parse(json);
3950
- const start = finiteNum(m.start);
3951
- const duration = finiteNum(m.duration);
3952
- if (start == null || duration == null || duration <= 0) continue;
3953
- const ease = typeof m.ease === "string" ? m.ease : "none";
3954
- const from = m.from && typeof m.from === "object" ? m.from : {};
3955
- const to = m.to && typeof m.to === "object" ? m.to : {};
3956
- const customEase = m.customEase;
3957
- let resolvedEase = ease;
3958
- if (customEase?.id && customEase?.data && win.CustomEase?.create) {
3959
- try {
3960
- gsap.registerPlugin?.(win.CustomEase);
3961
- win.CustomEase.create(customEase.id, customEase.data);
3962
- resolvedEase = customEase.id;
3963
- } catch {
3964
- }
3965
- }
3966
- fromTo.call(
3967
- tl,
3968
- el,
3969
- { ...from },
3970
- { ...to, duration, ease: resolvedEase, overwrite: "auto", immediateRender: false },
3971
- start
3972
- );
3973
- applied += 1;
3974
- } catch {
3975
- }
3976
- }
3977
- if (applied === 0) {
3978
- cachedMotionKey = "";
3979
- if (typeof tl.kill === "function")
3980
- tl.kill();
3981
- return;
3982
- }
3983
- cachedMotionKey = motionKey;
3984
- win.__timelines[MOTION_TL_KEY] = tl;
3985
- if (typeof tl.pause === "function") tl.pause();
3986
- if (typeof tl.totalTime === "function")
3987
- tl.totalTime(lastSeekTime, false);
3988
- };
3989
- const stripGsapTranslateFromTransform = (el) => {
3990
- const transform = el.style.getPropertyValue("transform");
3991
- if (!transform || transform === "none") return;
3992
- const win = el.ownerDocument.defaultView;
3993
- const MatrixCtor = win?.DOMMatrix;
3994
- if (!MatrixCtor) return;
3995
- try {
3996
- const m = new MatrixCtor(transform);
3997
- if (m.m41 === 0 && m.m42 === 0) return;
3998
- m.m41 = 0;
3999
- m.m42 = 0;
4000
- if (m.is2D && m.a === 1 && m.b === 0 && m.c === 0 && m.d === 1) {
4001
- el.style.removeProperty("transform");
4002
- } else {
4003
- el.style.setProperty("transform", m.toString());
4004
- }
4005
- } catch {
4006
- }
4007
- };
4008
- const reapplyAll = () => {
4009
- const offsetEls = document.querySelectorAll("[" + PATH_OFFSET_ATTR + '="true"]');
4010
- for (let i = 0; i < offsetEls.length; i++) {
4011
- const el = offsetEls[i];
4012
- if (!(el instanceof HTMLElement)) continue;
4013
- const x = el.style.getPropertyValue(OFFSET_X_PROP);
4014
- const y = el.style.getPropertyValue(OFFSET_Y_PROP);
4015
- if (x || y) {
4016
- el.style.setProperty(
4017
- "translate",
4018
- composeTranslate(
4019
- el,
4020
- "var(" + OFFSET_X_PROP + ", 0px)",
4021
- "var(" + OFFSET_Y_PROP + ", 0px)"
4022
- )
4023
- );
4024
- stripGsapTranslateFromTransform(el);
4025
- }
4026
- }
4027
- const boxSizeEls = document.querySelectorAll("[" + BOX_SIZE_ATTR + '="true"]');
4028
- for (let i = 0; i < boxSizeEls.length; i++) {
4029
- const el = boxSizeEls[i];
4030
- if (!(el instanceof HTMLElement)) continue;
4031
- const w = el.style.getPropertyValue(WIDTH_PROP);
4032
- const h = el.style.getPropertyValue(HEIGHT_PROP);
4033
- if (w) el.style.setProperty("width", w);
4034
- if (h) el.style.setProperty("height", h);
4035
- }
4036
- const rotEls = document.querySelectorAll("[" + ROTATION_ATTR + '="true"]');
4037
- for (let i = 0; i < rotEls.length; i++) {
4038
- const el = rotEls[i];
4039
- if (!(el instanceof HTMLElement)) continue;
4040
- const rot = el.style.getPropertyValue(ROTATION_PROP);
4041
- if (rot) {
4042
- el.style.setProperty("rotate", composeRotation(el, "var(" + ROTATION_PROP + ", 0deg)"));
4043
- stripGsapTranslateFromTransform(el);
4044
- }
4045
- }
4046
- reapplyMotionTimeline();
4047
- };
4048
- const runtimeWindow = window;
4049
- const isWrapped = (fn) => Boolean(fn[WRAPPED_PROP]);
4050
- const markWrapped = (fn) => {
4051
- try {
4052
- Object.defineProperty(fn, WRAPPED_PROP, {
4053
- configurable: false,
4054
- enumerable: false,
4055
- value: true
4056
- });
4057
- } catch {
4058
- try {
4059
- fn[WRAPPED_PROP] = true;
4060
- } catch {
4061
- }
4062
- }
4063
- };
4064
- const wrapFn = (get, set) => {
4065
- const fn = get();
4066
- if (typeof fn !== "function") return false;
4067
- const seek = fn;
4068
- if (isWrapped(seek)) {
4069
- reapplyAll();
4070
- return true;
4071
- }
4072
- const wrapped = function(time) {
4073
- lastSeekTime = typeof time === "number" && Number.isFinite(time) ? Math.max(0, time) : 0;
4074
- const result = seek.call(this, time);
4075
- reapplyAll();
4076
- return result;
4077
- };
4078
- markWrapped(wrapped);
4079
- set(wrapped);
4080
- reapplyAll();
4081
- return true;
4082
- };
4083
- const wrapSeekFunctions = () => {
4084
- const a = wrapFn(
4085
- () => runtimeWindow.__hf?.["seek"],
4086
- (fn) => {
4087
- if (runtimeWindow.__hf) runtimeWindow.__hf["seek"] = fn;
4088
- }
4089
- );
4090
- const b = wrapFn(
4091
- () => runtimeWindow.__player?.["renderSeek"],
4092
- (fn) => {
4093
- if (runtimeWindow.__player) runtimeWindow.__player["renderSeek"] = fn;
4094
- }
4095
- );
4096
- return a || b;
4097
- };
4098
- const installSeekTrap = (obj, key, getter, setter) => {
4099
- if (!obj) return;
4100
- try {
4101
- let current = obj[key];
4102
- Object.defineProperty(obj, key, {
4103
- configurable: true,
4104
- enumerable: true,
4105
- get() {
4106
- return current;
4107
- },
4108
- set(value) {
4109
- current = value;
4110
- if (typeof value === "function" && !isWrapped(value)) {
4111
- wrapFn(getter, setter);
4112
- }
4113
- }
4114
- });
4115
- } catch {
4116
- }
4117
- };
4118
- if (document.readyState === "loading") {
4119
- document.addEventListener("DOMContentLoaded", () => reapplyAll(), { once: true });
4120
- } else {
4121
- reapplyAll();
4122
- }
4123
- wrapSeekFunctions();
4124
- installSeekTrap(
4125
- runtimeWindow.__hf,
4126
- "seek",
4127
- () => runtimeWindow.__hf?.["seek"],
4128
- (fn) => {
4129
- if (runtimeWindow.__hf) runtimeWindow.__hf["seek"] = fn;
4130
- }
4131
- );
4132
- installSeekTrap(
4133
- runtimeWindow.__player,
4134
- "renderSeek",
4135
- () => runtimeWindow.__player?.["renderSeek"],
4136
- (fn) => {
4137
- if (runtimeWindow.__player) runtimeWindow.__player["renderSeek"] = fn;
4138
- }
4139
- );
4140
- let remaining = 120;
4141
- const interval = setInterval(() => {
4142
- wrapSeekFunctions();
4143
- remaining -= 1;
4144
- if (remaining <= 0) clearInterval(interval);
4145
- }, 50);
4146
- }
4147
- function studioManualEditsRenderRuntime(manifestContent, activeCompositionPath) {
4148
- const OFFSET_X_PROP = "--hf-studio-offset-x";
4149
- const OFFSET_Y_PROP = "--hf-studio-offset-y";
4150
- const WIDTH_PROP = "--hf-studio-width";
4151
- const HEIGHT_PROP = "--hf-studio-height";
4152
- const ROTATION_PROP = "--hf-studio-rotation";
4153
- const PATH_OFFSET_ATTR = "data-hf-studio-path-offset";
4154
- const BOX_SIZE_ATTR = "data-hf-studio-box-size";
4155
- const ROTATION_ATTR = "data-hf-studio-rotation";
4156
- const ORIGINAL_TRANSLATE_ATTR = "data-hf-studio-original-translate";
4157
- const ORIGINAL_ROTATE_ATTR = "data-hf-studio-original-rotate";
4158
- const WRAPPED_SEEK_PROP = "__hfStudioManualEditsWrapped";
4159
- const ROTATION_TRANSFORM_ORIGIN = "center center";
4160
- const finiteNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
4161
- const objectRecord = (value) => value && typeof value === "object" ? value : null;
4162
- const runtimeWindow = window;
4163
- const parsedManifest = (() => {
4164
- try {
4165
- return objectRecord(JSON.parse(manifestContent));
4166
- } catch {
4167
- return null;
4168
- }
4169
- })();
4170
- const manifestEdits = Array.isArray(parsedManifest?.edits) ? parsedManifest.edits : [];
4171
- if (manifestEdits.length === 0) return;
4172
- const sourceFileForElement = (element) => {
4173
- let current = element;
4174
- while (current) {
4175
- const sourceFile = current.getAttribute("data-composition-file") ?? current.getAttribute("data-composition-src");
4176
- if (sourceFile) return sourceFile;
4177
- current = current.parentElement;
4178
- }
4179
- return activeCompositionPath ?? "index.html";
4180
- };
4181
- const elementMatchesSourceFile = (element, sourceFile) => sourceFileForElement(element) === sourceFile;
4182
- const styleUsesStudioOffset = (value) => value.includes(OFFSET_X_PROP) || value.includes(OFFSET_Y_PROP);
4183
- const styleUsesStudioRotation = (value) => value.includes(ROTATION_PROP);
4184
- const splitTopLevelWhitespace = (value) => {
4185
- const parts = [];
4186
- let depth = 0;
4187
- let current = "";
4188
- for (const char of value.trim()) {
4189
- if (char === "(") depth += 1;
4190
- if (char === ")") depth = Math.max(0, depth - 1);
4191
- if (/\s/.test(char) && depth === 0) {
4192
- if (current) parts.push(current);
4193
- current = "";
4194
- } else {
4195
- current += char;
4196
- }
4197
- }
4198
- if (current) parts.push(current);
4199
- return parts;
4200
- };
4201
- const composeTranslate = (element, x, y) => {
4202
- const original = element.getAttribute(ORIGINAL_TRANSLATE_ATTR)?.trim();
4203
- if (!original || original === "none") return `${x} ${y}`;
4204
- const parts = splitTopLevelWhitespace(original);
4205
- if (parts.length === 1) return `calc(${parts[0]} + ${x}) ${y}`;
4206
- if (parts.length === 2) return `calc(${parts[0]} + ${x}) calc(${parts[1]} + ${y})`;
4207
- if (parts.length === 3) {
4208
- return `calc(${parts[0]} + ${x}) calc(${parts[1]} + ${y}) ${parts[2]}`;
4209
- }
4210
- return `${x} ${y}`;
4211
- };
4212
- const readStyleOrComputed = (element, property) => {
4213
- try {
4214
- return element.style.getPropertyValue(property) || getComputedStyle(element).getPropertyValue(property);
4215
- } catch {
4216
- return element.style.getPropertyValue(property);
4217
- }
4218
- };
4219
- const readTransformLonghandBase = (element, property) => {
4220
- const value = readStyleOrComputed(element, property).trim();
4221
- return value === "none" ? "" : value;
4222
- };
4223
- const preparePathOffsetBase = (element) => {
4224
- const currentTranslate = readTransformLonghandBase(element, "translate");
4225
- const hasMarker = element.hasAttribute(PATH_OFFSET_ATTR);
4226
- const wasResetByAnimation = !styleUsesStudioOffset(currentTranslate);
4227
- if (!hasMarker) {
4228
- element.setAttribute(ORIGINAL_TRANSLATE_ATTR, wasResetByAnimation ? currentTranslate : "");
4229
- } else if (wasResetByAnimation) {
4230
- element.setAttribute(ORIGINAL_TRANSLATE_ATTR, currentTranslate);
4231
- }
4232
- };
4233
- const prepareRotationBase = (element) => {
4234
- const currentRotate = readTransformLonghandBase(element, "rotate");
4235
- const hasMarker = element.hasAttribute(ROTATION_ATTR);
4236
- const wasResetByAnimation = !styleUsesStudioRotation(currentRotate);
4237
- if (!hasMarker) {
4238
- element.setAttribute(ORIGINAL_ROTATE_ATTR, wasResetByAnimation ? currentRotate : "");
4239
- } else if (wasResetByAnimation) {
4240
- element.setAttribute(ORIGINAL_ROTATE_ATTR, currentRotate);
4241
- }
4242
- };
4243
- const querySelectorCandidates = (selector) => {
4244
- const isCandidate = (element) => element instanceof HTMLElement;
4245
- const className = selector.match(/^\.([A-Za-z0-9_-]+)$/)?.[1];
4246
- if (className) {
4247
- return Array.from(document.getElementsByTagName("*")).filter(
4248
- (element) => isCandidate(element) && element.classList.contains(className)
4249
- );
4250
- }
4251
- if (/^[A-Za-z][A-Za-z0-9-]*$/.test(selector)) {
4252
- return Array.from(document.getElementsByTagName(selector)).filter(isCandidate);
4253
- }
4254
- return Array.from(document.querySelectorAll(selector)).filter(isCandidate);
4255
- };
4256
- const resolveTarget = (edit) => {
4257
- const targetRecord = objectRecord(edit.target);
4258
- if (!targetRecord) return null;
4259
- const sourceFile = typeof targetRecord.sourceFile === "string" ? targetRecord.sourceFile : "";
4260
- if (!sourceFile) return null;
4261
- const id = typeof targetRecord.id === "string" ? targetRecord.id : "";
4262
- if (id) {
4263
- const byId = document.getElementById(id);
4264
- if (byId instanceof HTMLElement && elementMatchesSourceFile(byId, sourceFile)) return byId;
4265
- const matchesById = [
4266
- document.documentElement,
4267
- ...Array.from(document.getElementsByTagName("*"))
4268
- ].filter(
4269
- (element) => element instanceof HTMLElement && element.id === id && elementMatchesSourceFile(element, sourceFile)
4270
- );
4271
- if (matchesById[0]) return matchesById[0];
4272
- }
4273
- const selector = typeof targetRecord.selector === "string" ? targetRecord.selector : "";
4274
- if (!selector) return null;
4275
- try {
4276
- const matches = querySelectorCandidates(selector).filter(
4277
- (element) => elementMatchesSourceFile(element, sourceFile)
4278
- );
4279
- const selectorIndex = finiteNumber(targetRecord.selectorIndex) ?? 0;
4280
- return matches[Math.max(0, Math.floor(selectorIndex))] ?? null;
4281
- } catch {
4282
- return null;
4283
- }
4284
- };
4285
- const roundRotationAngle = (angle) => Math.round(angle * 10) / 10;
4286
- const isSimpleRotateAngle = (value) => /^-?(?:\d+(?:\.\d+)?|\.\d+)(?:deg|rad|turn|grad)$/.test(value.trim());
4287
- const composeRotation = (element, rotationValue) => {
4288
- const original = element.getAttribute(ORIGINAL_ROTATE_ATTR)?.trim();
4289
- if (!original || original === "none" || !isSimpleRotateAngle(original)) {
4290
- return rotationValue;
4291
- }
4292
- return `calc(${original} + ${rotationValue})`;
4293
- };
4294
- const applyPathOffset = (element, edit) => {
4295
- const x = finiteNumber(edit.x);
4296
- const y = finiteNumber(edit.y);
4297
- if (x == null || y == null) return;
4298
- preparePathOffsetBase(element);
4299
- element.setAttribute(PATH_OFFSET_ATTR, "true");
4300
- element.style.setProperty(OFFSET_X_PROP, `${Math.round(x)}px`);
4301
- element.style.setProperty(OFFSET_Y_PROP, `${Math.round(y)}px`);
4302
- element.style.setProperty(
4303
- "translate",
4304
- composeTranslate(element, `var(${OFFSET_X_PROP}, 0px)`, `var(${OFFSET_Y_PROP}, 0px)`)
4305
- );
4306
- };
4307
- const readParentFlexBasisPixels = (element, size) => {
4308
- const parent = element.parentElement;
4309
- if (!parent) return null;
4310
- const styles = getComputedStyle(parent);
4311
- if (styles.display !== "flex" && styles.display !== "inline-flex") return null;
4312
- return Math.round(
4313
- Math.max(1, styles.flexDirection.startsWith("column") ? size.height : size.width)
4314
- );
4315
- };
4316
- const applyBoxSize = (element, edit) => {
4317
- const width = finiteNumber(edit.width);
4318
- const height = finiteNumber(edit.height);
4319
- if (width == null || height == null || width <= 0 || height <= 0) return;
4320
- const rounded = {
4321
- width: Math.round(Math.max(1, width)),
4322
- height: Math.round(Math.max(1, height))
4323
- };
4324
- element.setAttribute(BOX_SIZE_ATTR, "true");
4325
- element.style.setProperty(WIDTH_PROP, `${rounded.width}px`);
4326
- element.style.setProperty(HEIGHT_PROP, `${rounded.height}px`);
4327
- element.style.setProperty("box-sizing", "border-box");
4328
- element.style.setProperty("width", `${rounded.width}px`);
4329
- element.style.setProperty("height", `${rounded.height}px`);
4330
- element.style.setProperty("min-width", "0px");
4331
- element.style.setProperty("min-height", "0px");
4332
- element.style.setProperty("max-width", "none");
4333
- element.style.setProperty("max-height", "none");
4334
- const flexBasis = readParentFlexBasisPixels(element, rounded);
4335
- if (flexBasis != null) {
4336
- element.style.setProperty("flex-basis", `${flexBasis}px`);
4337
- element.style.setProperty("flex-grow", "0");
4338
- element.style.setProperty("flex-shrink", "0");
4339
- }
4340
- if (getComputedStyle(element).display === "inline") {
4341
- element.style.setProperty("display", "inline-block");
4342
- }
4343
- };
4344
- const applyRotation = (element, edit) => {
4345
- const angle = finiteNumber(edit.angle);
4346
- if (angle == null) return;
4347
- prepareRotationBase(element);
4348
- element.setAttribute(ROTATION_ATTR, "true");
4349
- element.style.setProperty(ROTATION_PROP, `${roundRotationAngle(angle)}deg`);
4350
- element.style.setProperty("transform-origin", ROTATION_TRANSFORM_ORIGIN);
4351
- element.style.setProperty("rotate", composeRotation(element, `var(${ROTATION_PROP}, 0deg)`));
4352
- };
4353
- const applyManifest = () => {
4354
- let applied = 0;
4355
- for (const edit of manifestEdits) {
4356
- const editRecord = objectRecord(edit);
4357
- if (!editRecord) continue;
4358
- const element = resolveTarget(editRecord);
4359
- if (!element) continue;
4360
- if (editRecord.kind === "path-offset") applyPathOffset(element, editRecord);
4361
- if (editRecord.kind === "box-size") applyBoxSize(element, editRecord);
4362
- if (editRecord.kind === "rotation") applyRotation(element, editRecord);
4363
- applied += 1;
4364
- }
4365
- return applied;
4366
- };
4367
- runtimeWindow.__hfStudioManualEditsApply = applyManifest;
4368
- const markWrapped = (fn) => {
4369
- try {
4370
- Object.defineProperty(fn, WRAPPED_SEEK_PROP, {
4371
- configurable: false,
4372
- enumerable: false,
4373
- value: true
4374
- });
4375
- } catch {
4376
- try {
4377
- fn[WRAPPED_SEEK_PROP] = true;
4378
- } catch {
4379
- }
4380
- }
4381
- };
4382
- const isWrapped = (fn) => Boolean(fn[WRAPPED_SEEK_PROP]);
4383
- const wrapFunction = (get, set) => {
4384
- const fn = get();
4385
- if (!fn) return false;
4386
- const seek = fn;
4387
- if (isWrapped(seek)) {
4388
- applyManifest();
4389
- return true;
4390
- }
4391
- const wrappedSeek = function(time) {
4392
- const result = seek.call(this, time);
4393
- applyManifest();
4394
- return result;
4395
- };
4396
- markWrapped(wrappedSeek);
4397
- set(wrappedSeek);
4398
- applyManifest();
4399
- return true;
4400
- };
4401
- const wrapSeekFunctions = () => {
4402
- const wrappedHfSeek = wrapFunction(
4403
- () => runtimeWindow.__hf?.seek,
4404
- (fn) => {
4405
- if (runtimeWindow.__hf) runtimeWindow.__hf.seek = fn;
4406
- }
4407
- );
4408
- const wrappedPlayerRenderSeek = wrapFunction(
4409
- () => runtimeWindow.__player?.renderSeek,
4410
- (fn) => {
4411
- if (runtimeWindow.__player) runtimeWindow.__player.renderSeek = fn;
4412
- }
4413
- );
4414
- return wrappedHfSeek || wrappedPlayerRenderSeek;
4415
- };
4416
- if (document.readyState === "loading") {
4417
- document.addEventListener("DOMContentLoaded", () => applyManifest(), { once: true });
4418
- } else {
4419
- applyManifest();
4420
- }
4421
- wrapSeekFunctions();
4422
- let remainingSeekWrapAttempts = 120;
4423
- const seekWrapInterval = setInterval(() => {
4424
- wrapSeekFunctions();
4425
- remainingSeekWrapAttempts -= 1;
4426
- if (remainingSeekWrapAttempts <= 0) clearInterval(seekWrapInterval);
4427
- }, 50);
4428
- }
4429
-
4430
- // src/routes/thumbnail.ts
4431
3782
  var THUMBNAIL_CACHE_VERSION = "v4";
4432
3783
  function registerThumbnailRoutes(api, adapter) {
4433
3784
  api.get("/projects/:id/thumbnail/*", async (c) => {
@@ -4457,8 +3808,8 @@ function registerThumbnailRoutes(api, adapter) {
4457
3808
  let sourceMtime = 0;
4458
3809
  let sourceKey = "";
4459
3810
  const htmlFile = join11(project.dir, compPath);
4460
- if (existsSync7(htmlFile)) {
4461
- const html = readFileSync10(htmlFile, "utf-8");
3811
+ if (existsSync8(htmlFile)) {
3812
+ const html = readFileSync11(htmlFile, "utf-8");
4462
3813
  sourceKey = `_${createHash4("sha1").update(html).digest("hex").slice(0, 16)}`;
4463
3814
  sourceMtime = Math.round(statSync4(htmlFile).mtimeMs);
4464
3815
  if (!vpWidth) {
@@ -4470,15 +3821,15 @@ function registerThumbnailRoutes(api, adapter) {
4470
3821
  }
4471
3822
  const manualEditsFile = join11(project.dir, STUDIO_MANUAL_EDITS_PATH);
4472
3823
  let manualEditsKey = "";
4473
- if (existsSync7(manualEditsFile)) {
4474
- const manualEditsContent = readFileSync10(manualEditsFile, "utf-8");
3824
+ if (existsSync8(manualEditsFile)) {
3825
+ const manualEditsContent = readFileSync11(manualEditsFile, "utf-8");
4475
3826
  manualEditsKey = `_${createHash4("sha1").update(manualEditsContent).digest("hex").slice(0, 16)}`;
4476
3827
  sourceMtime = Math.max(sourceMtime, Math.round(statSync4(manualEditsFile).mtimeMs));
4477
3828
  }
4478
3829
  const motionFile = join11(project.dir, STUDIO_MOTION_PATH);
4479
3830
  let motionKey = "";
4480
- if (existsSync7(motionFile)) {
4481
- const motionContent = readFileSync10(motionFile, "utf-8");
3831
+ if (existsSync8(motionFile)) {
3832
+ const motionContent = readFileSync11(motionFile, "utf-8");
4482
3833
  motionKey = `_${createHash4("sha1").update(motionContent).digest("hex").slice(0, 16)}`;
4483
3834
  sourceMtime = Math.max(sourceMtime, Math.round(statSync4(motionFile).mtimeMs));
4484
3835
  }
@@ -4488,8 +3839,8 @@ function registerThumbnailRoutes(api, adapter) {
4488
3839
  const urlVersionKey = urlVersion ? `_${urlVersion.replace(/[^a-zA-Z0-9_-]+/g, "_").slice(0, 32)}` : "";
4489
3840
  const cacheKey = `${THUMBNAIL_CACHE_VERSION}${urlVersionKey}${manualEditsKey}${motionKey}${sourceKey}_${format}_${compPath.replace(/\//g, "_")}_${compW}x${compH}_${sourceMtime}_${seekTime.toFixed(2)}${selectorKey}.${format === "png" ? "png" : "jpg"}`;
4490
3841
  const cachePath = join11(cacheDir, cacheKey);
4491
- if (existsSync7(cachePath)) {
4492
- return new Response(new Uint8Array(readFileSync10(cachePath)), {
3842
+ if (existsSync8(cachePath)) {
3843
+ return new Response(new Uint8Array(readFileSync11(cachePath)), {
4493
3844
  headers: { "Content-Type": contentType, "Cache-Control": "no-cache" }
4494
3845
  });
4495
3846
  }
@@ -4511,7 +3862,7 @@ function registerThumbnailRoutes(api, adapter) {
4511
3862
  500
4512
3863
  );
4513
3864
  }
4514
- if (!existsSync7(cacheDir)) mkdirSync5(cacheDir, { recursive: true });
3865
+ if (!existsSync8(cacheDir)) mkdirSync5(cacheDir, { recursive: true });
4515
3866
  writeFileSync6(cachePath, buffer);
4516
3867
  return new Response(new Uint8Array(buffer), {
4517
3868
  headers: { "Content-Type": contentType, "Cache-Control": "no-cache" }
@@ -4524,7 +3875,7 @@ function registerThumbnailRoutes(api, adapter) {
4524
3875
  }
4525
3876
 
4526
3877
  // src/routes/waveform.ts
4527
- import { existsSync as existsSync8, readFileSync as readFileSync11, writeFileSync as writeFileSync7, mkdirSync as mkdirSync6 } from "fs";
3878
+ import { existsSync as existsSync9, readFileSync as readFileSync12, writeFileSync as writeFileSync7, mkdirSync as mkdirSync6 } from "fs";
4528
3879
  import { join as join12 } from "path";
4529
3880
  function registerWaveformRoutes(api, adapter) {
4530
3881
  api.get("/projects/:id/waveform/*", async (c) => {
@@ -4534,12 +3885,12 @@ function registerWaveformRoutes(api, adapter) {
4534
3885
  c.req.path.replace(`/projects/${project.id}/waveform/`, "").split("?")[0] ?? ""
4535
3886
  );
4536
3887
  const audioPath = join12(project.dir, assetPath);
4537
- if (!existsSync8(audioPath)) return c.json({ error: "file not found" }, 404);
3888
+ if (!existsSync9(audioPath)) return c.json({ error: "file not found" }, 404);
4538
3889
  const cacheDir = join12(project.dir, ".waveform-cache");
4539
3890
  const cachePath = join12(cacheDir, buildWaveformCacheKey(assetPath));
4540
- if (existsSync8(cachePath)) {
3891
+ if (existsSync9(cachePath)) {
4541
3892
  try {
4542
- const peaks2 = JSON.parse(readFileSync11(cachePath, "utf-8"));
3893
+ const peaks2 = JSON.parse(readFileSync12(cachePath, "utf-8"));
4543
3894
  return c.json({ peaks: peaks2 });
4544
3895
  } catch {
4545
3896
  }
@@ -4823,102 +4174,29 @@ function registerSelectionRoutes(api, adapter) {
4823
4174
 
4824
4175
  // src/routes/media.ts
4825
4176
  import { streamSSE as streamSSE2 } from "hono/streaming";
4826
- import { existsSync as existsSync9, mkdirSync as mkdirSync7 } from "fs";
4827
- import { basename as basename2, dirname as dirname2, extname as extname3, join as join13 } from "path";
4828
-
4829
- // src/helpers/mediaMetadata.ts
4830
- import { spawnSync as spawnSync2 } from "child_process";
4831
- import { extname as extname2 } from "path";
4832
- var VIDEO_EXT2 = /* @__PURE__ */ new Set([".mp4", ".mov", ".webm", ".mkv", ".avi", ".m4v"]);
4833
- var IMAGE_EXT = /* @__PURE__ */ new Set([".jpg", ".jpeg", ".png", ".webp", ".avif"]);
4834
- var AUDIO_EXT2 = /* @__PURE__ */ new Set([".mp3", ".wav", ".ogg", ".m4a", ".aac"]);
4835
- function lower(value) {
4836
- return value?.toLowerCase() ?? "";
4837
- }
4838
- function inferKindFromPath(path) {
4839
- const ext = extname2(path).toLowerCase();
4840
- if (VIDEO_EXT2.has(ext)) return "video";
4841
- if (IMAGE_EXT.has(ext)) return "image";
4842
- if (AUDIO_EXT2.has(ext)) return "audio";
4843
- return "unknown";
4844
- }
4845
- function colorLabel(input) {
4846
- if (input.isHdr) {
4847
- if (input.hdrTransfer === "pq") return "HDR PQ";
4848
- if (input.hdrTransfer === "hlg") return "HDR HLG";
4849
- return "HDR";
4850
- }
4851
- if (input.colorPrimaries.includes("bt709") || input.colorSpace.includes("bt709") || input.colorTransfer.includes("bt709")) {
4852
- return "SDR Rec.709";
4853
- }
4854
- return "SDR/unknown";
4855
- }
4856
- function classifyMediaColor(stream) {
4857
- const colorPrimaries = lower(stream?.color_primaries);
4858
- const colorSpace = lower(stream?.color_space);
4859
- const colorTransfer = lower(stream?.color_transfer);
4860
- const isHdr = colorPrimaries.includes("bt2020") || colorSpace.includes("bt2020") || colorTransfer === "smpte2084" || colorTransfer === "arib-std-b67";
4861
- const hdrTransfer = isHdr ? colorTransfer === "smpte2084" ? "pq" : colorTransfer === "arib-std-b67" ? "hlg" : "unknown" : null;
4862
- return {
4863
- dynamicRange: stream ? isHdr ? "hdr" : "sdr" : "unknown",
4864
- hdrTransfer,
4865
- label: stream ? colorLabel({ isHdr, hdrTransfer, colorPrimaries, colorSpace, colorTransfer }) : "Unknown",
4866
- isHdr,
4867
- codecName: stream?.codec_name,
4868
- profile: stream?.profile,
4869
- pixelFormat: stream?.pix_fmt,
4870
- colorSpace: stream?.color_space,
4871
- colorTransfer: stream?.color_transfer,
4872
- colorPrimaries: stream?.color_primaries,
4873
- bitsPerRawSample: stream?.bits_per_raw_sample
4874
- };
4875
- }
4876
- function probeMediaMetadata(filePath, runner = spawnSync2) {
4877
- const kind = inferKindFromPath(filePath);
4878
- if (kind === "audio" || kind === "unknown") {
4879
- return { kind, color: classifyMediaColor(null) };
4880
- }
4881
- const result = runner(
4882
- "ffprobe",
4883
- [
4884
- "-v",
4885
- "error",
4886
- "-show_entries",
4887
- "stream=codec_type,codec_name,profile,pix_fmt,color_space,color_transfer,color_primaries,bits_per_raw_sample",
4888
- "-of",
4889
- "json",
4890
- filePath
4891
- ],
4892
- { timeout: 15e3, maxBuffer: 1024 * 1024 }
4893
- );
4894
- if (result.error?.code === "ENOENT") {
4895
- return { kind, color: classifyMediaColor(null), probeError: "ffprobe unavailable" };
4896
- }
4897
- if (result.status !== 0) {
4898
- return { kind, color: classifyMediaColor(null), probeError: "ffprobe failed" };
4899
- }
4900
- try {
4901
- const parsed = JSON.parse(String(result.stdout || "{}"));
4902
- const stream = parsed.streams?.find(
4903
- (item) => kind === "image" ? item.codec_type === "video" : item.codec_type === kind
4904
- );
4905
- return { kind, color: classifyMediaColor(stream) };
4906
- } catch {
4907
- return { kind, color: classifyMediaColor(null), probeError: "ffprobe returned invalid json" };
4908
- }
4909
- }
4910
-
4911
- // src/routes/media.ts
4912
- var VIDEO_EXTENSIONS = /* @__PURE__ */ new Set([".mp4", ".mov", ".webm", ".mkv", ".avi"]);
4177
+ import { existsSync as existsSync10, mkdirSync as mkdirSync7 } from "fs";
4178
+ import { basename as basename2, dirname as dirname3, extname as extname2, join as join13 } from "path";
4179
+ var VIDEO_EXTENSIONS = /* @__PURE__ */ new Set([
4180
+ ".mp4",
4181
+ ".mov",
4182
+ ".webm",
4183
+ ".mkv",
4184
+ ".avi",
4185
+ ".m4v",
4186
+ ".mxf",
4187
+ ".mts",
4188
+ ".m2ts",
4189
+ ".ts"
4190
+ ]);
4913
4191
  var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".jpg", ".jpeg", ".png", ".webp"]);
4914
4192
  var VIDEO_OUTPUT_EXTENSIONS = /* @__PURE__ */ new Set([".webm", ".mov"]);
4915
4193
  var QUALITIES = /* @__PURE__ */ new Set(["fast", "balanced", "best"]);
4916
4194
  var DEVICES = /* @__PURE__ */ new Set(["auto", "cpu", "coreml", "cuda"]);
4917
4195
  function isVideoPath(path) {
4918
- return VIDEO_EXTENSIONS.has(extname3(path).toLowerCase());
4196
+ return VIDEO_EXTENSIONS.has(extname2(path).toLowerCase());
4919
4197
  }
4920
4198
  function isImagePath(path) {
4921
- return IMAGE_EXTENSIONS.has(extname3(path).toLowerCase());
4199
+ return IMAGE_EXTENSIONS.has(extname2(path).toLowerCase());
4922
4200
  }
4923
4201
  function normalizeProjectAssetPath(path) {
4924
4202
  return path.trim().replace(/^[.]\//, "").replace(/[?#].*$/, "");
@@ -4927,14 +4205,14 @@ function containsNullByte(path) {
4927
4205
  return path.includes("\0");
4928
4206
  }
4929
4207
  function slugFileBase(path) {
4930
- const name = basename2(path, extname3(path)).replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
4208
+ const name = basename2(path, extname2(path)).replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
4931
4209
  return name || "media";
4932
4210
  }
4933
4211
  function uniqueAssetPath(projectDir, assetPath) {
4934
- const ext = extname3(assetPath);
4212
+ const ext = extname2(assetPath);
4935
4213
  const withoutExt = assetPath.slice(0, -ext.length);
4936
4214
  let candidate = assetPath;
4937
- for (let index = 2; existsSync9(join13(projectDir, candidate)); index++) {
4215
+ for (let index = 2; existsSync10(join13(projectDir, candidate)); index++) {
4938
4216
  candidate = `${withoutExt}-${index}${ext}`;
4939
4217
  }
4940
4218
  return candidate;
@@ -4985,8 +4263,8 @@ function registerMediaRoutes(api, adapter, options = {}) {
4985
4263
  }
4986
4264
  const filePath = resolveWithinProject(project.dir, assetPath);
4987
4265
  if (!filePath) return c.json({ error: "forbidden" }, 403);
4988
- if (!existsSync9(filePath)) return c.json({ error: "media not found" }, 404);
4989
- return c.json({ path: assetPath, metadata: readMediaMetadata(filePath) });
4266
+ if (!existsSync10(filePath)) return c.json({ error: "media not found" }, 404);
4267
+ return c.json({ path: assetPath, metadata: await readMediaMetadata(filePath) });
4990
4268
  });
4991
4269
  api.post(
4992
4270
  "/projects/:id/media/remove-background",
@@ -5007,7 +4285,7 @@ function registerMediaRoutes(api, adapter, options = {}) {
5007
4285
  }
5008
4286
  const inputPath = resolveWithinProject(project.dir, inputAssetPath);
5009
4287
  if (!inputPath) return c.json({ error: "forbidden" }, 403);
5010
- if (!existsSync9(inputPath)) return c.json({ error: "input media not found" }, 404);
4288
+ if (!existsSync10(inputPath)) return c.json({ error: "input media not found" }, 404);
5011
4289
  const inputIsVideo = isVideoPath(inputAssetPath);
5012
4290
  const inputIsImage = isImagePath(inputAssetPath);
5013
4291
  if (!inputIsVideo && !inputIsImage) {
@@ -5023,10 +4301,10 @@ function registerMediaRoutes(api, adapter, options = {}) {
5023
4301
  const outputAssetPath = requestedOutput ? uniqueAssetPath(project.dir, requestedOutput) : defaultOutputPath(project.dir, inputAssetPath);
5024
4302
  const outputPath = resolveWithinProject(project.dir, outputAssetPath);
5025
4303
  if (!outputPath) return c.json({ error: "forbidden" }, 403);
5026
- if (inputIsVideo && !VIDEO_OUTPUT_EXTENSIONS.has(extname3(outputAssetPath).toLowerCase())) {
4304
+ if (inputIsVideo && !VIDEO_OUTPUT_EXTENSIONS.has(extname2(outputAssetPath).toLowerCase())) {
5027
4305
  return c.json({ error: "video background removal output must be .webm or .mov" }, 400);
5028
4306
  }
5029
- if (inputIsImage && extname3(outputAssetPath).toLowerCase() !== ".png") {
4307
+ if (inputIsImage && extname2(outputAssetPath).toLowerCase() !== ".png") {
5030
4308
  return c.json({ error: "image background removal output must be .png" }, 400);
5031
4309
  }
5032
4310
  let backgroundOutputAssetPath;
@@ -5041,8 +4319,8 @@ function registerMediaRoutes(api, adapter, options = {}) {
5041
4319
  return c.json({ error: "forbidden" }, 403);
5042
4320
  }
5043
4321
  }
5044
- mkdirSync7(dirname2(outputPath), { recursive: true });
5045
- if (backgroundOutputPath) mkdirSync7(dirname2(backgroundOutputPath), { recursive: true });
4322
+ mkdirSync7(dirname3(outputPath), { recursive: true });
4323
+ if (backgroundOutputPath) mkdirSync7(dirname3(backgroundOutputPath), { recursive: true });
5046
4324
  const jobId = makeJobId(project.id, mediaJobs);
5047
4325
  const state = adapter.startBackgroundRemoval({
5048
4326
  project,
@@ -5099,14 +4377,14 @@ function registerMediaRoutes(api, adapter, options = {}) {
5099
4377
  }
5100
4378
 
5101
4379
  // src/routes/globalAssets.ts
5102
- import { existsSync as existsSync10, readFileSync as readFileSync12 } from "fs";
4380
+ import { existsSync as existsSync11, readFileSync as readFileSync13 } from "fs";
5103
4381
  import { homedir } from "os";
5104
4382
  import { join as join14 } from "path";
5105
4383
  function readGlobalAssets(home = homedir()) {
5106
4384
  const manifestPath = join14(home, ".media", "manifest.jsonl");
5107
- if (!existsSync10(manifestPath)) return [];
4385
+ if (!existsSync11(manifestPath)) return [];
5108
4386
  const out = [];
5109
- for (const line of readFileSync12(manifestPath, "utf8").split("\n")) {
4387
+ for (const line of readFileSync13(manifestPath, "utf8").split("\n")) {
5110
4388
  if (!line.trim()) continue;
5111
4389
  try {
5112
4390
  const rec = JSON.parse(line);
@@ -5142,29 +4420,6 @@ function createStudioApi(adapter) {
5142
4420
  return api;
5143
4421
  }
5144
4422
 
5145
- // src/helpers/screenshotClip.ts
5146
- function getElementScreenshotClip(selector, selectorIndex) {
5147
- const matches = Array.from(document.querySelectorAll(selector)).filter(
5148
- (el2) => el2 instanceof HTMLElement
5149
- );
5150
- const safeIndex = Math.max(0, Math.min(matches.length - 1, Math.floor(selectorIndex ?? 0)));
5151
- const el = matches[safeIndex] ?? null;
5152
- if (!(el instanceof HTMLElement)) return void 0;
5153
- const rect = el.getBoundingClientRect();
5154
- if (rect.width < 4 || rect.height < 4) return void 0;
5155
- const pad = 8;
5156
- const x = Math.max(0, rect.left - pad);
5157
- const y = Math.max(0, rect.top - pad);
5158
- const maxWidth = window.innerWidth - x;
5159
- const maxHeight = window.innerHeight - y;
5160
- return {
5161
- x,
5162
- y,
5163
- width: Math.max(1, Math.min(rect.width + pad * 2, maxWidth)),
5164
- height: Math.max(1, Math.min(rect.height + pad * 2, maxHeight))
5165
- };
5166
- }
5167
-
5168
4423
  // src/helpers/backgroundRemovalJob.ts
5169
4424
  function createBackgroundRemovalJob(opts, render) {
5170
4425
  const state = {