@hyperframes/studio-server 0.8.107 → 0.8.108
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-TEYDBVSK.js +361 -0
- package/dist/chunk-TEYDBVSK.js.map +1 -0
- package/dist/helpers/loudness.d.ts +87 -0
- package/dist/helpers/loudness.js +29 -0
- package/dist/helpers/loudness.js.map +1 -0
- package/dist/helpers/mediaProxyPreview.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +588 -57
- package/dist/index.js.map +1 -1
- package/dist/{mediaProxyPreview-rleU3r_Q.d.ts → mediaProxyPreview-CCxjB3rf.d.ts} +1 -0
- package/package.json +7 -3
package/dist/index.js
CHANGED
|
@@ -47,6 +47,13 @@ import {
|
|
|
47
47
|
import {
|
|
48
48
|
getElementScreenshotClip
|
|
49
49
|
} from "./chunk-W2SBTCO2.js";
|
|
50
|
+
import {
|
|
51
|
+
DEFAULT_TARGET_LUFS,
|
|
52
|
+
absoluteLoudnessPlan,
|
|
53
|
+
measureAudio,
|
|
54
|
+
requiredFfmpeg,
|
|
55
|
+
resolveLocalAudioPath
|
|
56
|
+
} from "./chunk-TEYDBVSK.js";
|
|
50
57
|
import {
|
|
51
58
|
injectMediaCodecMap,
|
|
52
59
|
isAutoProxyEnabled,
|
|
@@ -1248,7 +1255,7 @@ function bakeGroupTransformIntoMembers(html, groupId, members, groupCenter) {
|
|
|
1248
1255
|
const rad = grot * Math.PI / 180;
|
|
1249
1256
|
const cos = Math.cos(rad);
|
|
1250
1257
|
const sin = Math.sin(rad);
|
|
1251
|
-
const
|
|
1258
|
+
const round32 = (n) => Math.round(n * 1e3) / 1e3;
|
|
1252
1259
|
let script = block.scriptText;
|
|
1253
1260
|
for (const m of members) {
|
|
1254
1261
|
const memberSel = `#${m.id}`;
|
|
@@ -1265,27 +1272,27 @@ function bakeGroupTransformIntoMembers(html, groupId, members, groupCenter) {
|
|
|
1265
1272
|
const visY = groupCenter.cy + gscale * (sin * dx + cos * dy) + gy;
|
|
1266
1273
|
const newProps = {
|
|
1267
1274
|
...mProps,
|
|
1268
|
-
x:
|
|
1269
|
-
y:
|
|
1275
|
+
x: round32(visX - m.cx),
|
|
1276
|
+
y: round32(visY - m.cy)
|
|
1270
1277
|
};
|
|
1271
1278
|
if (gz !== 0) newProps.z = (typeof mProps.z === "number" ? mProps.z : 0) + gz;
|
|
1272
1279
|
if (grot !== 0) {
|
|
1273
|
-
newProps.rotation =
|
|
1280
|
+
newProps.rotation = round32(
|
|
1274
1281
|
(typeof mProps.rotation === "number" ? mProps.rotation : 0) + grot
|
|
1275
1282
|
);
|
|
1276
1283
|
}
|
|
1277
1284
|
if (gscale !== 1) {
|
|
1278
|
-
newProps.scale =
|
|
1285
|
+
newProps.scale = round32((typeof mProps.scale === "number" ? mProps.scale : 1) * gscale);
|
|
1279
1286
|
}
|
|
1280
1287
|
const pivoted = /* @__PURE__ */ new Set(["x", "y", "z", "rotation", "scale"]);
|
|
1281
1288
|
for (const [k, v] of Object.entries(gt)) {
|
|
1282
1289
|
if (pivoted.has(k) || typeof v !== "number") continue;
|
|
1283
1290
|
if (k === "scaleX" || k === "scaleY") {
|
|
1284
|
-
if (v !== 1) newProps[k] =
|
|
1291
|
+
if (v !== 1) newProps[k] = round32((typeof mProps[k] === "number" ? mProps[k] : 1) * v);
|
|
1285
1292
|
} else if (k === "transformPerspective") {
|
|
1286
1293
|
if (typeof mProps[k] !== "number") newProps[k] = v;
|
|
1287
1294
|
} else if (v !== 0) {
|
|
1288
|
-
newProps[k] =
|
|
1295
|
+
newProps[k] = round32((typeof mProps[k] === "number" ? mProps[k] : 0) + v);
|
|
1289
1296
|
}
|
|
1290
1297
|
}
|
|
1291
1298
|
for (const s of [...sets].reverse()) {
|
|
@@ -4096,7 +4103,8 @@ function registerRenderRoutes(api, adapter) {
|
|
|
4096
4103
|
progress: current.progress,
|
|
4097
4104
|
status: current.status,
|
|
4098
4105
|
stage: current.stage,
|
|
4099
|
-
error: current.error
|
|
4106
|
+
error: current.error,
|
|
4107
|
+
...current.status === "complete" && current.audioLoweredDb !== void 0 ? { audioLoweredDb: current.audioLoweredDb } : {}
|
|
4100
4108
|
})
|
|
4101
4109
|
});
|
|
4102
4110
|
if (current.status !== "rendering") break;
|
|
@@ -4205,11 +4213,13 @@ function registerRenderRoutes(api, adapter) {
|
|
|
4205
4213
|
const metaPath = join10(rendersDir, `${rid}.meta.json`);
|
|
4206
4214
|
let status = "complete";
|
|
4207
4215
|
let durationMs;
|
|
4216
|
+
let audioLoweredDb;
|
|
4208
4217
|
if (existsSync6(metaPath)) {
|
|
4209
4218
|
try {
|
|
4210
4219
|
const meta = JSON.parse(readFileSync7(metaPath, "utf-8"));
|
|
4211
4220
|
if (meta.status === "failed" && !existsSync6(fp)) status = "failed";
|
|
4212
4221
|
if (meta.durationMs) durationMs = meta.durationMs;
|
|
4222
|
+
if (typeof meta.audioLoweredDb === "number") audioLoweredDb = meta.audioLoweredDb;
|
|
4213
4223
|
} catch {
|
|
4214
4224
|
}
|
|
4215
4225
|
}
|
|
@@ -4219,7 +4229,8 @@ function registerRenderRoutes(api, adapter) {
|
|
|
4219
4229
|
size: stat2.size,
|
|
4220
4230
|
createdAt: stat2.mtimeMs,
|
|
4221
4231
|
status,
|
|
4222
|
-
durationMs
|
|
4232
|
+
durationMs,
|
|
4233
|
+
...audioLoweredDb !== void 0 ? { audioLoweredDb } : {}
|
|
4223
4234
|
};
|
|
4224
4235
|
}).sort((a, b) => b.createdAt - a.createdAt);
|
|
4225
4236
|
for (const file of files) {
|
|
@@ -4259,10 +4270,10 @@ var ThumbnailGenerationCoordinator = class {
|
|
|
4259
4270
|
if (signal.aborted) return Promise.reject(new DOMException("Aborted", "AbortError"));
|
|
4260
4271
|
let entry = this.entries.get(key);
|
|
4261
4272
|
if (!entry) {
|
|
4262
|
-
let
|
|
4273
|
+
let resolve7;
|
|
4263
4274
|
let reject;
|
|
4264
4275
|
const promise = new Promise((resolvePromise, rejectPromise) => {
|
|
4265
|
-
|
|
4276
|
+
resolve7 = resolvePromise;
|
|
4266
4277
|
reject = rejectPromise;
|
|
4267
4278
|
});
|
|
4268
4279
|
entry = {
|
|
@@ -4272,7 +4283,7 @@ var ThumbnailGenerationCoordinator = class {
|
|
|
4272
4283
|
state: "queued",
|
|
4273
4284
|
work,
|
|
4274
4285
|
promise,
|
|
4275
|
-
resolve:
|
|
4286
|
+
resolve: resolve7,
|
|
4276
4287
|
reject
|
|
4277
4288
|
};
|
|
4278
4289
|
this.entries.set(key, entry);
|
|
@@ -4286,7 +4297,7 @@ var ThumbnailGenerationCoordinator = class {
|
|
|
4286
4297
|
return /* @__PURE__ */ new Set([...this.entries.keys(), ...[...this.activeEntries].map((entry) => entry.key)]);
|
|
4287
4298
|
}
|
|
4288
4299
|
lease(entry, signal) {
|
|
4289
|
-
return new Promise((
|
|
4300
|
+
return new Promise((resolve7, reject) => {
|
|
4290
4301
|
let released = false;
|
|
4291
4302
|
const release = () => {
|
|
4292
4303
|
if (released) return;
|
|
@@ -4310,7 +4321,7 @@ var ThumbnailGenerationCoordinator = class {
|
|
|
4310
4321
|
entry.promise.then(
|
|
4311
4322
|
(value) => {
|
|
4312
4323
|
release();
|
|
4313
|
-
|
|
4324
|
+
resolve7(value);
|
|
4314
4325
|
},
|
|
4315
4326
|
(reason) => {
|
|
4316
4327
|
release();
|
|
@@ -4387,14 +4398,14 @@ async function generateThumbnail(path, signal) {
|
|
|
4387
4398
|
async function resolveSource(adapter, id, url) {
|
|
4388
4399
|
const project = await adapter.resolveProject(id);
|
|
4389
4400
|
if (!project) throw new HTTPException(404);
|
|
4390
|
-
let
|
|
4401
|
+
let relative5;
|
|
4391
4402
|
try {
|
|
4392
|
-
|
|
4403
|
+
relative5 = requestSubPath(url, "projects/:id/image-thumbnail");
|
|
4393
4404
|
} catch {
|
|
4394
4405
|
throw new HTTPException(400);
|
|
4395
4406
|
}
|
|
4396
|
-
if (!/\.jpe?g$/i.test(
|
|
4397
|
-
const path = pinWithinProject(project.dir,
|
|
4407
|
+
if (!/\.jpe?g$/i.test(relative5)) throw new HTTPException(415);
|
|
4408
|
+
const path = pinWithinProject(project.dir, relative5);
|
|
4398
4409
|
if (!path) throw new HTTPException(404);
|
|
4399
4410
|
const info = await stat(path).catch(() => {
|
|
4400
4411
|
throw new HTTPException(404);
|
|
@@ -4762,6 +4773,523 @@ function registerWaveformRoutes(api, adapter) {
|
|
|
4762
4773
|
});
|
|
4763
4774
|
}
|
|
4764
4775
|
|
|
4776
|
+
// src/routes/freezeFrame.ts
|
|
4777
|
+
import { execFile } from "child_process";
|
|
4778
|
+
import { existsSync as existsSync8, readFileSync as readFileSync11, statSync as statSync8 } from "fs";
|
|
4779
|
+
import { dirname as dirname5, join as join13, relative as relative4, sep as sep2 } from "path";
|
|
4780
|
+
import { findFfBinary as findFfBinary3 } from "@hyperframes/parsers/ff-binaries";
|
|
4781
|
+
import { replaceFileAtomically as replaceFileAtomically3 } from "@hyperframes/core/atomic-file";
|
|
4782
|
+
|
|
4783
|
+
// src/helpers/freezeFrame.ts
|
|
4784
|
+
import { createHash as createHash4, randomBytes } from "crypto";
|
|
4785
|
+
import { ensureHfIds as ensureHfIds3 } from "@hyperframes/parsers/hf-ids";
|
|
4786
|
+
import { readMediaOffsetSeconds } from "@hyperframes/parsers/media-duration";
|
|
4787
|
+
import { resolveRateSpec, sourceTimeAt } from "@hyperframes/core/speed-ramp";
|
|
4788
|
+
var FREEZE_HOLD_SECONDS = 2;
|
|
4789
|
+
var LINK_ATTR = "data-link";
|
|
4790
|
+
var EPSILON = 1e-3;
|
|
4791
|
+
var round3 = (value) => Math.round(value * 1e3) / 1e3;
|
|
4792
|
+
function numberAttr(el, name) {
|
|
4793
|
+
const raw = el.getAttribute(name);
|
|
4794
|
+
if (raw === null || raw.trim() === "") return null;
|
|
4795
|
+
const value = Number(raw);
|
|
4796
|
+
return Number.isFinite(value) ? value : null;
|
|
4797
|
+
}
|
|
4798
|
+
function trackOf(el) {
|
|
4799
|
+
return numberAttr(el, "data-track-index") ?? numberAttr(el, "data-layer") ?? 0;
|
|
4800
|
+
}
|
|
4801
|
+
function freezeFrameMediaTime(input) {
|
|
4802
|
+
const rate = resolveRateSpec(input.automation, input.playbackRate);
|
|
4803
|
+
return input.mediaStart + sourceTimeAt(rate, Math.max(0, input.playhead - input.clipStart));
|
|
4804
|
+
}
|
|
4805
|
+
function freezeExtractArgs(src, mediaTime, output) {
|
|
4806
|
+
return ["-n", "-ss", String(round3(mediaTime)), "-i", src, "-frames:v", "1", output];
|
|
4807
|
+
}
|
|
4808
|
+
var randomStillToken = () => randomBytes(4).toString("hex");
|
|
4809
|
+
function freezeStillFileName(clipId, playhead, token = randomStillToken()) {
|
|
4810
|
+
const stem = clipId.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 48) || "clip";
|
|
4811
|
+
const idHash = createHash4("sha256").update(clipId).digest("hex").slice(0, 10);
|
|
4812
|
+
return `${stem}-${idHash}-${Math.round(playhead * 1e3)}-${token}.png`;
|
|
4813
|
+
}
|
|
4814
|
+
function readFreezeSource(html, target, playhead) {
|
|
4815
|
+
const { document } = parseSourceDocument(html);
|
|
4816
|
+
const el = findTargetElement(document, target);
|
|
4817
|
+
if (!el || el.tagName.toLowerCase() !== "video") return null;
|
|
4818
|
+
const src = el.getAttribute("src");
|
|
4819
|
+
const start = numberAttr(el, "data-start");
|
|
4820
|
+
const duration = numberAttr(el, "data-duration");
|
|
4821
|
+
if (!src || start === null || duration === null) return null;
|
|
4822
|
+
if (playhead <= start + EPSILON || playhead >= start + duration - EPSILON) return null;
|
|
4823
|
+
return {
|
|
4824
|
+
id: el.getAttribute("id") || "clip",
|
|
4825
|
+
src,
|
|
4826
|
+
mediaTime: freezeFrameMediaTime({
|
|
4827
|
+
clipStart: start,
|
|
4828
|
+
playhead,
|
|
4829
|
+
mediaStart: readMediaOffsetSeconds((name) => el.getAttribute(name)),
|
|
4830
|
+
playbackRate: numberAttr(el, "data-playback-rate") ?? 1,
|
|
4831
|
+
automation: el.getAttribute("data-automation")
|
|
4832
|
+
})
|
|
4833
|
+
};
|
|
4834
|
+
}
|
|
4835
|
+
function targetOf(el) {
|
|
4836
|
+
const hfId = el.getAttribute("data-hf-id");
|
|
4837
|
+
return { ...hfId ? { hfId } : {}, id: el.getAttribute("id") };
|
|
4838
|
+
}
|
|
4839
|
+
function spans(el, time) {
|
|
4840
|
+
const start = numberAttr(el, "data-start");
|
|
4841
|
+
const duration = numberAttr(el, "data-duration");
|
|
4842
|
+
if (start === null || duration === null) return false;
|
|
4843
|
+
return time > start + EPSILON && time < start + duration - EPSILON;
|
|
4844
|
+
}
|
|
4845
|
+
function uniqueId(document, base) {
|
|
4846
|
+
let id = base;
|
|
4847
|
+
for (let n = 2; document.getElementById(id); n++) id = `${base}-${n}`;
|
|
4848
|
+
return id;
|
|
4849
|
+
}
|
|
4850
|
+
function shiftTracks(document, tracks, from, by) {
|
|
4851
|
+
for (const el of document.querySelectorAll("[data-start]")) {
|
|
4852
|
+
if (el.hasAttribute("data-composition-id") || !tracks.has(trackOf(el))) continue;
|
|
4853
|
+
const start = numberAttr(el, "data-start");
|
|
4854
|
+
if (start === null || start < from - EPSILON) continue;
|
|
4855
|
+
el.setAttribute("data-start", String(round3(start + by)));
|
|
4856
|
+
}
|
|
4857
|
+
}
|
|
4858
|
+
function growRootToFit(document) {
|
|
4859
|
+
const root = document.querySelector("[data-composition-id]");
|
|
4860
|
+
const rootDuration = root ? numberAttr(root, "data-duration") : null;
|
|
4861
|
+
if (!root || rootDuration === null) return;
|
|
4862
|
+
let end = rootDuration;
|
|
4863
|
+
for (const el of root.querySelectorAll("[data-start][data-duration]")) {
|
|
4864
|
+
const start = numberAttr(el, "data-start");
|
|
4865
|
+
const duration = numberAttr(el, "data-duration");
|
|
4866
|
+
if (start !== null && duration !== null) end = Math.max(end, start + duration);
|
|
4867
|
+
}
|
|
4868
|
+
if (end > rootDuration + EPSILON) root.setAttribute("data-duration", String(round3(end)));
|
|
4869
|
+
}
|
|
4870
|
+
var COPIED_PICTURE_ATTRS = ["class", "style", "data-color-grading"];
|
|
4871
|
+
function buildStill(document, video, input) {
|
|
4872
|
+
const img = document.createElement("img");
|
|
4873
|
+
img.setAttribute("id", input.id);
|
|
4874
|
+
for (const name of COPIED_PICTURE_ATTRS) {
|
|
4875
|
+
const value = video.getAttribute(name);
|
|
4876
|
+
if (value !== null) img.setAttribute(name, value);
|
|
4877
|
+
}
|
|
4878
|
+
if (!img.classList.contains("clip")) img.classList.add("clip");
|
|
4879
|
+
img.setAttribute("src", input.imageSrc);
|
|
4880
|
+
img.setAttribute("alt", "");
|
|
4881
|
+
img.setAttribute("data-start", String(round3(input.playhead)));
|
|
4882
|
+
img.setAttribute("data-duration", String(input.holdSeconds));
|
|
4883
|
+
img.setAttribute("data-track-index", String(trackOf(video)));
|
|
4884
|
+
img.setAttribute("data-timeline-label", "Freeze");
|
|
4885
|
+
return img;
|
|
4886
|
+
}
|
|
4887
|
+
function linkedPartners(document, video) {
|
|
4888
|
+
const link = video.getAttribute(LINK_ATTR);
|
|
4889
|
+
if (!link) return [];
|
|
4890
|
+
return Array.from(document.querySelectorAll(`[${LINK_ATTR}]`)).filter(
|
|
4891
|
+
(el) => el !== video && el.getAttribute(LINK_ATTR) === link
|
|
4892
|
+
);
|
|
4893
|
+
}
|
|
4894
|
+
function splitAll(html, elements, time) {
|
|
4895
|
+
const rightHalfIds = [];
|
|
4896
|
+
let next = html;
|
|
4897
|
+
for (const el of elements) {
|
|
4898
|
+
const split = splitElementInHtml(
|
|
4899
|
+
next,
|
|
4900
|
+
targetOf(el),
|
|
4901
|
+
time,
|
|
4902
|
+
`${el.getAttribute("id") || "clip"}-split`,
|
|
4903
|
+
{
|
|
4904
|
+
start: numberAttr(el, "data-start") ?? 0,
|
|
4905
|
+
duration: numberAttr(el, "data-duration") ?? 0,
|
|
4906
|
+
track: trackOf(el)
|
|
4907
|
+
}
|
|
4908
|
+
);
|
|
4909
|
+
if (!split.matched || !split.newId) return null;
|
|
4910
|
+
next = split.html;
|
|
4911
|
+
rightHalfIds.push(split.newId);
|
|
4912
|
+
}
|
|
4913
|
+
return { html: next, rightHalfIds };
|
|
4914
|
+
}
|
|
4915
|
+
function relinkRightHalves(document, link, ids) {
|
|
4916
|
+
if (!link) return;
|
|
4917
|
+
const rightLink = uniqueLink(document, link);
|
|
4918
|
+
for (const id of ids) document.getElementById(id)?.setAttribute(LINK_ATTR, rightLink);
|
|
4919
|
+
}
|
|
4920
|
+
function applyFreezeFrameToHtml(html, input) {
|
|
4921
|
+
const hold = input.holdSeconds ?? FREEZE_HOLD_SECONDS;
|
|
4922
|
+
const { document: before } = parseSourceDocument(html);
|
|
4923
|
+
const video = findTargetElement(before, input.target);
|
|
4924
|
+
if (!video || !isHTMLElement(video) || !spans(video, input.playhead)) return null;
|
|
4925
|
+
const partners = linkedPartners(before, video);
|
|
4926
|
+
const tracks = /* @__PURE__ */ new Set([trackOf(video), ...partners.map(trackOf)]);
|
|
4927
|
+
const cut = splitAll(
|
|
4928
|
+
html,
|
|
4929
|
+
[video, ...partners.filter((el) => spans(el, input.playhead))],
|
|
4930
|
+
input.playhead
|
|
4931
|
+
);
|
|
4932
|
+
if (!cut) return null;
|
|
4933
|
+
const { document, wrappedFragment } = parseSourceDocument(cut.html);
|
|
4934
|
+
relinkRightHalves(document, video.getAttribute(LINK_ATTR), cut.rightHalfIds);
|
|
4935
|
+
shiftTracks(document, tracks, input.playhead, hold);
|
|
4936
|
+
const leftHalf = findTargetElement(document, input.target);
|
|
4937
|
+
if (!leftHalf?.parentElement) return null;
|
|
4938
|
+
const freezeId = uniqueId(document, `${video.getAttribute("id") || "clip"}-freeze`);
|
|
4939
|
+
const still = buildStill(document, leftHalf, {
|
|
4940
|
+
id: freezeId,
|
|
4941
|
+
imageSrc: input.imageSrc,
|
|
4942
|
+
playhead: input.playhead,
|
|
4943
|
+
holdSeconds: hold
|
|
4944
|
+
});
|
|
4945
|
+
leftHalf.parentElement.insertBefore(still, leftHalf.nextSibling);
|
|
4946
|
+
growRootToFit(document);
|
|
4947
|
+
return {
|
|
4948
|
+
html: ensureHfIds3(wrappedFragment ? document.body.innerHTML || "" : document.toString()),
|
|
4949
|
+
freezeId
|
|
4950
|
+
};
|
|
4951
|
+
}
|
|
4952
|
+
function uniqueLink(document, base) {
|
|
4953
|
+
const taken = new Set(
|
|
4954
|
+
Array.from(document.querySelectorAll(`[${LINK_ATTR}]`)).map((el) => el.getAttribute(LINK_ATTR))
|
|
4955
|
+
);
|
|
4956
|
+
let candidate = `${base}-2`;
|
|
4957
|
+
for (let n = 3; taken.has(candidate); n++) candidate = `${base}-${n}`;
|
|
4958
|
+
return candidate;
|
|
4959
|
+
}
|
|
4960
|
+
|
|
4961
|
+
// src/routes/freezeFrame.ts
|
|
4962
|
+
var EXTRACT_TIMEOUT_MS = 3e4;
|
|
4963
|
+
var ffmpegExtractor = (args) => {
|
|
4964
|
+
const ffmpeg = findFfBinary3("ffmpeg", { configuredMustExist: true });
|
|
4965
|
+
if (!ffmpeg) return Promise.resolve({ ok: false, error: "ffmpeg not found" });
|
|
4966
|
+
return new Promise((resolvePromise) => {
|
|
4967
|
+
execFile(
|
|
4968
|
+
ffmpeg,
|
|
4969
|
+
args,
|
|
4970
|
+
{ timeout: EXTRACT_TIMEOUT_MS, windowsHide: true },
|
|
4971
|
+
(error, _o, stderr) => resolvePromise(
|
|
4972
|
+
error ? { ok: false, error: String(stderr || error.message) } : { ok: true }
|
|
4973
|
+
)
|
|
4974
|
+
);
|
|
4975
|
+
});
|
|
4976
|
+
};
|
|
4977
|
+
function isFreezeFrameRequest(value) {
|
|
4978
|
+
if (typeof value !== "object" || value === null) return false;
|
|
4979
|
+
const body = value;
|
|
4980
|
+
return typeof body.path === "string" && body.path.length > 0 && typeof body.expectedVersion === "string" && typeof body.target === "object" && body.target !== null && typeof body.playhead === "number" && Number.isFinite(body.playhead);
|
|
4981
|
+
}
|
|
4982
|
+
var FREEZE_DIR = ["assets", "freeze"];
|
|
4983
|
+
function readExpected(absPath, expectedVersion) {
|
|
4984
|
+
let content;
|
|
4985
|
+
try {
|
|
4986
|
+
content = readFileSync11(absPath, "utf-8");
|
|
4987
|
+
} catch {
|
|
4988
|
+
return { error: "not found", status: 404 };
|
|
4989
|
+
}
|
|
4990
|
+
return fileContentVersion(content) === expectedVersion ? { content } : { error: "file conflict", status: 409 };
|
|
4991
|
+
}
|
|
4992
|
+
async function extractStill(projectDir, absPath, source, playhead, tools) {
|
|
4993
|
+
const fileDir = dirname5(absPath);
|
|
4994
|
+
const mediaPath = pinWithinProject(projectDir, relative4(projectDir, join13(fileDir, source.src)));
|
|
4995
|
+
if (!mediaPath) return { error: `forbidden media path: ${source.src}`, status: 403 };
|
|
4996
|
+
const freezeDir = join13(projectDir, ...FREEZE_DIR);
|
|
4997
|
+
const fileName = freezeStillFileName(source.id, playhead, tools.stillToken());
|
|
4998
|
+
mkdirWithinProject(projectDir, freezeDir);
|
|
4999
|
+
const imagePath = pinWithinProject(projectDir, join13(...FREEZE_DIR, fileName));
|
|
5000
|
+
if (!imagePath || dirname5(imagePath) !== freezeDir) {
|
|
5001
|
+
return { error: `forbidden freeze path: ${fileName}`, status: 403 };
|
|
5002
|
+
}
|
|
5003
|
+
if (existsSync8(imagePath))
|
|
5004
|
+
return { error: `freeze still already exists: ${fileName}`, status: 409 };
|
|
5005
|
+
const extracted = await tools.extract(freezeExtractArgs(mediaPath, source.mediaTime, imagePath));
|
|
5006
|
+
if (!extracted.ok) {
|
|
5007
|
+
return {
|
|
5008
|
+
error: `Could not extract the frame: ${extracted.error ?? "ffmpeg failed"}`,
|
|
5009
|
+
status: 500
|
|
5010
|
+
};
|
|
5011
|
+
}
|
|
5012
|
+
const depth = relative4(projectDir, fileDir).split(sep2).filter(Boolean).length;
|
|
5013
|
+
return { imageSrc: `${"../".repeat(depth)}${FREEZE_DIR.join("/")}/${fileName}` };
|
|
5014
|
+
}
|
|
5015
|
+
function writeFolded(projectDir, absPath, path, before, after, options) {
|
|
5016
|
+
if (readFileSync11(absPath, "utf-8") !== before) {
|
|
5017
|
+
return { error: `file conflict: ${path}`, status: 409 };
|
|
5018
|
+
}
|
|
5019
|
+
const backup = snapshotBeforeWrite(projectDir, absPath);
|
|
5020
|
+
if (backup.error) return { error: `Failed to create backup: ${backup.error}`, status: 500 };
|
|
5021
|
+
const writeToken = createWriteToken(options.token);
|
|
5022
|
+
replaceFileAtomically3(absPath, after, statSync8(absPath).mode);
|
|
5023
|
+
const version = fileContentVersion(after);
|
|
5024
|
+
recordFileWriteReceipt(absPath, { path, version, writeToken, overwrote: before });
|
|
5025
|
+
return { version, writeToken, backupPath: backupPathForResponse(projectDir, backup.backupPath) };
|
|
5026
|
+
}
|
|
5027
|
+
function registerFreezeFrameRoutes(api, adapter, extract = ffmpegExtractor, stillToken = randomStillToken) {
|
|
5028
|
+
api.post("/projects/:id/file-mutations/freeze-frame", async (c) => {
|
|
5029
|
+
const body = await c.req.json().catch(() => null);
|
|
5030
|
+
if (!isFreezeFrameRequest(body)) {
|
|
5031
|
+
return c.json({ error: "path, expectedVersion, target and playhead required" }, 400);
|
|
5032
|
+
}
|
|
5033
|
+
const project = await adapter.resolveProject(c.req.param("id"));
|
|
5034
|
+
if (!project) return c.json({ error: "not found" }, 404);
|
|
5035
|
+
const absPath = pinWithinProject(project.dir, body.path);
|
|
5036
|
+
if (!absPath) return c.json({ error: `forbidden path: ${body.path}` }, 403);
|
|
5037
|
+
const read = readExpected(absPath, body.expectedVersion);
|
|
5038
|
+
if ("error" in read) return c.json({ error: `${read.error}: ${body.path}` }, read.status);
|
|
5039
|
+
const before = read.content;
|
|
5040
|
+
const source = readFreezeSource(before, body.target, body.playhead);
|
|
5041
|
+
if (!source) return c.json({ error: "Move the playhead inside a video clip to freeze" }, 400);
|
|
5042
|
+
const still = await extractStill(project.dir, absPath, source, body.playhead, {
|
|
5043
|
+
extract,
|
|
5044
|
+
stillToken
|
|
5045
|
+
});
|
|
5046
|
+
if ("error" in still) return c.json({ error: still.error }, still.status);
|
|
5047
|
+
const { imageSrc } = still;
|
|
5048
|
+
const folded = applyFreezeFrameToHtml(before, {
|
|
5049
|
+
target: body.target,
|
|
5050
|
+
playhead: body.playhead,
|
|
5051
|
+
imageSrc
|
|
5052
|
+
});
|
|
5053
|
+
if (!folded) return c.json({ error: "Freeze target was not found in the file" }, 400);
|
|
5054
|
+
const written = writeFolded(project.dir, absPath, body.path, before, folded.html, {
|
|
5055
|
+
token: body.transactionToken ?? c.req.header("X-Hyperframes-Write-Token")
|
|
5056
|
+
});
|
|
5057
|
+
if ("error" in written) return c.json({ error: written.error }, written.status);
|
|
5058
|
+
const { version, writeToken, backupPath } = written;
|
|
5059
|
+
return c.json({
|
|
5060
|
+
ok: true,
|
|
5061
|
+
path: body.path,
|
|
5062
|
+
before,
|
|
5063
|
+
after: folded.html,
|
|
5064
|
+
version,
|
|
5065
|
+
writeToken,
|
|
5066
|
+
backupPath,
|
|
5067
|
+
freezeId: folded.freezeId,
|
|
5068
|
+
imageSrc
|
|
5069
|
+
});
|
|
5070
|
+
});
|
|
5071
|
+
}
|
|
5072
|
+
|
|
5073
|
+
// src/routes/loudness.ts
|
|
5074
|
+
import { existsSync as existsSync9 } from "fs";
|
|
5075
|
+
var defaultMeasure = (file, window) => measureAudio(requiredFfmpeg(), file, window);
|
|
5076
|
+
var InvalidField = class extends Error {
|
|
5077
|
+
};
|
|
5078
|
+
function numberField(body, name, fallback, valid) {
|
|
5079
|
+
const raw = body[name];
|
|
5080
|
+
if (raw === void 0 || raw === null) return fallback;
|
|
5081
|
+
if (typeof raw === "number" && Number.isFinite(raw) && valid(raw)) return raw;
|
|
5082
|
+
throw new InvalidField(`${name} is out of range`);
|
|
5083
|
+
}
|
|
5084
|
+
function readNormalizeRequest(body) {
|
|
5085
|
+
const src = typeof body.src === "string" ? body.src.trim() : "";
|
|
5086
|
+
if (!src) throw new InvalidField("src is required");
|
|
5087
|
+
const duration = numberField(body, "duration", 0, (v) => v > 0);
|
|
5088
|
+
return {
|
|
5089
|
+
id: typeof body.id === "string" && body.id ? body.id : src,
|
|
5090
|
+
src,
|
|
5091
|
+
window: {
|
|
5092
|
+
mediaStart: numberField(body, "mediaStart", 0, (v) => v >= 0),
|
|
5093
|
+
duration: duration > 0 ? duration : null,
|
|
5094
|
+
playbackRate: numberField(body, "playbackRate", 1, (v) => v > 0)
|
|
5095
|
+
},
|
|
5096
|
+
volume: numberField(body, "volume", 1, (v) => v >= 0),
|
|
5097
|
+
targetLufs: numberField(body, "targetLufs", DEFAULT_TARGET_LUFS, (v) => v < 0)
|
|
5098
|
+
};
|
|
5099
|
+
}
|
|
5100
|
+
function parseNormalizeRequest(body) {
|
|
5101
|
+
try {
|
|
5102
|
+
return readNormalizeRequest(body);
|
|
5103
|
+
} catch (error) {
|
|
5104
|
+
if (error instanceof InvalidField) return error.message;
|
|
5105
|
+
throw error;
|
|
5106
|
+
}
|
|
5107
|
+
}
|
|
5108
|
+
function isRecord(value) {
|
|
5109
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5110
|
+
}
|
|
5111
|
+
function registerLoudnessRoutes(api, adapter, measure = defaultMeasure) {
|
|
5112
|
+
api.post("/projects/:id/loudness/normalize", async (c) => {
|
|
5113
|
+
const project = await adapter.resolveProject(c.req.param("id"));
|
|
5114
|
+
if (!project) return c.json({ error: "not found" }, 404);
|
|
5115
|
+
const body = await c.req.json().catch(() => null);
|
|
5116
|
+
if (!isRecord(body)) return c.json({ error: "invalid body" }, 400);
|
|
5117
|
+
const request = parseNormalizeRequest(body);
|
|
5118
|
+
if (typeof request === "string") return c.json({ error: request }, 400);
|
|
5119
|
+
let file;
|
|
5120
|
+
try {
|
|
5121
|
+
file = resolveLocalAudioPath(project.dir, request.src);
|
|
5122
|
+
} catch (error) {
|
|
5123
|
+
return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
5124
|
+
}
|
|
5125
|
+
if (!existsSync9(file)) return c.json({ error: "file not found" }, 404);
|
|
5126
|
+
try {
|
|
5127
|
+
const measurement = await measure(file, request.window);
|
|
5128
|
+
const plan = absoluteLoudnessPlan(
|
|
5129
|
+
{ id: request.id, volume: request.volume, ...measurement },
|
|
5130
|
+
request.targetLufs
|
|
5131
|
+
);
|
|
5132
|
+
return c.json({ plan });
|
|
5133
|
+
} catch (error) {
|
|
5134
|
+
return c.json({ error: error instanceof Error ? error.message : String(error) }, 422);
|
|
5135
|
+
}
|
|
5136
|
+
});
|
|
5137
|
+
}
|
|
5138
|
+
|
|
5139
|
+
// src/routes/peaks.ts
|
|
5140
|
+
import { existsSync as existsSync10, readFileSync as readFileSync12, statSync as statSync9 } from "fs";
|
|
5141
|
+
import { join as join14, resolve as resolve6 } from "path";
|
|
5142
|
+
import { isWithinProjectRoot as isWithinProjectRoot3 } from "@hyperframes/parsers/asset-resolution";
|
|
5143
|
+
|
|
5144
|
+
// src/helpers/peakMap.ts
|
|
5145
|
+
import { execFile as execFile2, spawn as spawn2 } from "child_process";
|
|
5146
|
+
import { promisify } from "util";
|
|
5147
|
+
import { findFfBinary as findFfBinary4 } from "@hyperframes/parsers/ff-binaries";
|
|
5148
|
+
var PEAK_BIN_SECONDS = 0.05;
|
|
5149
|
+
var PEAK_SAMPLE_RATE = 48e3;
|
|
5150
|
+
var execFileAsync = promisify(execFile2);
|
|
5151
|
+
var PEAK_MAP_CACHE_VERSION = "peaks-v2";
|
|
5152
|
+
function createPeakAccumulator(sampleRate = PEAK_SAMPLE_RATE, binSeconds = PEAK_BIN_SECONDS, channels = 2) {
|
|
5153
|
+
return {
|
|
5154
|
+
bins: [],
|
|
5155
|
+
frame: 0,
|
|
5156
|
+
carry: Buffer.alloc(0),
|
|
5157
|
+
framesPerBin: Math.max(1, Math.round(sampleRate * binSeconds)),
|
|
5158
|
+
channels: Math.max(1, channels)
|
|
5159
|
+
};
|
|
5160
|
+
}
|
|
5161
|
+
function framePeak(data, offset, channels) {
|
|
5162
|
+
let peak = 0;
|
|
5163
|
+
for (let ch = 0; ch < channels; ch++) {
|
|
5164
|
+
peak = Math.max(peak, Math.abs(data.readFloatLE(offset + ch * 4)));
|
|
5165
|
+
}
|
|
5166
|
+
return peak;
|
|
5167
|
+
}
|
|
5168
|
+
function accumulatePeaks(acc, chunk) {
|
|
5169
|
+
const data = acc.carry.length > 0 ? Buffer.concat([acc.carry, chunk]) : chunk;
|
|
5170
|
+
const frameBytes = 4 * acc.channels;
|
|
5171
|
+
const whole = data.length - data.length % frameBytes;
|
|
5172
|
+
for (let offset = 0; offset < whole; offset += frameBytes) {
|
|
5173
|
+
const peak = framePeak(data, offset, acc.channels);
|
|
5174
|
+
const bin = Math.floor(acc.frame / acc.framesPerBin);
|
|
5175
|
+
if (acc.bins.length <= bin) acc.bins.push(0);
|
|
5176
|
+
if (peak > (acc.bins[bin] ?? 0)) acc.bins[bin] = peak;
|
|
5177
|
+
acc.frame += 1;
|
|
5178
|
+
}
|
|
5179
|
+
acc.carry = Buffer.from(data.subarray(whole));
|
|
5180
|
+
}
|
|
5181
|
+
function finishPeaks(acc) {
|
|
5182
|
+
return acc.bins.map((peak) => Number(peak.toFixed(4)));
|
|
5183
|
+
}
|
|
5184
|
+
async function probeChannels(mediaPath) {
|
|
5185
|
+
const { stdout } = await execFileAsync(
|
|
5186
|
+
findFfBinary4("ffprobe") ?? "ffprobe",
|
|
5187
|
+
[
|
|
5188
|
+
"-v",
|
|
5189
|
+
"error",
|
|
5190
|
+
"-select_streams",
|
|
5191
|
+
"a:0",
|
|
5192
|
+
"-show_entries",
|
|
5193
|
+
"stream=channels",
|
|
5194
|
+
"-of",
|
|
5195
|
+
"csv=p=0",
|
|
5196
|
+
"--",
|
|
5197
|
+
mediaPath
|
|
5198
|
+
],
|
|
5199
|
+
{ encoding: "utf8", timeout: 3e4, windowsHide: true }
|
|
5200
|
+
);
|
|
5201
|
+
const channels = Number.parseInt(stdout.trim(), 10);
|
|
5202
|
+
if (!Number.isFinite(channels) || channels < 1) throw new Error("no audio stream");
|
|
5203
|
+
return channels;
|
|
5204
|
+
}
|
|
5205
|
+
async function decodePeakMap(mediaPath) {
|
|
5206
|
+
const channels = await probeChannels(mediaPath);
|
|
5207
|
+
return new Promise((resolvePromise, reject) => {
|
|
5208
|
+
const proc = spawn2(
|
|
5209
|
+
findFfBinary4("ffmpeg") ?? "ffmpeg",
|
|
5210
|
+
[
|
|
5211
|
+
"-v",
|
|
5212
|
+
"error",
|
|
5213
|
+
"-i",
|
|
5214
|
+
mediaPath,
|
|
5215
|
+
"-map",
|
|
5216
|
+
"0:a:0",
|
|
5217
|
+
"-vn",
|
|
5218
|
+
"-ar",
|
|
5219
|
+
String(PEAK_SAMPLE_RATE),
|
|
5220
|
+
"-f",
|
|
5221
|
+
"f32le",
|
|
5222
|
+
"pipe:1"
|
|
5223
|
+
],
|
|
5224
|
+
{ stdio: ["ignore", "pipe", "ignore"], windowsHide: true }
|
|
5225
|
+
);
|
|
5226
|
+
const acc = createPeakAccumulator(PEAK_SAMPLE_RATE, PEAK_BIN_SECONDS, channels);
|
|
5227
|
+
proc.stdout?.on("data", (chunk) => accumulatePeaks(acc, chunk));
|
|
5228
|
+
proc.on("close", (code) => {
|
|
5229
|
+
if (code !== 0) {
|
|
5230
|
+
reject(new Error(`ffmpeg failed decoding audio (exit ${code})`));
|
|
5231
|
+
return;
|
|
5232
|
+
}
|
|
5233
|
+
if (acc.frame === 0) {
|
|
5234
|
+
reject(new Error("ffmpeg produced no audio samples"));
|
|
5235
|
+
return;
|
|
5236
|
+
}
|
|
5237
|
+
resolvePromise(finishPeaks(acc));
|
|
5238
|
+
});
|
|
5239
|
+
proc.on("error", reject);
|
|
5240
|
+
});
|
|
5241
|
+
}
|
|
5242
|
+
function buildPeakMapCacheKey(assetPath, fingerprint) {
|
|
5243
|
+
const name = assetPath.replace(/[/\\]/g, "_");
|
|
5244
|
+
return `${PEAK_MAP_CACHE_VERSION}_${name}_${fingerprint.size}-${Math.round(fingerprint.mtimeMs)}.json`;
|
|
5245
|
+
}
|
|
5246
|
+
|
|
5247
|
+
// src/routes/peaks.ts
|
|
5248
|
+
var decodesInFlight = /* @__PURE__ */ new Map();
|
|
5249
|
+
function decodeOnce(cachePath, mediaPath, decode) {
|
|
5250
|
+
const pending = decodesInFlight.get(cachePath);
|
|
5251
|
+
if (pending) return pending;
|
|
5252
|
+
const started = decode(mediaPath).finally(() => decodesInFlight.delete(cachePath));
|
|
5253
|
+
decodesInFlight.set(cachePath, started);
|
|
5254
|
+
return started;
|
|
5255
|
+
}
|
|
5256
|
+
function readCachedBins(cacheDir, cachePath) {
|
|
5257
|
+
try {
|
|
5258
|
+
if (!isWaveformCacheDirectory(cacheDir) || !existsSync10(cachePath)) return null;
|
|
5259
|
+
const parsed = JSON.parse(readFileSync12(cachePath, "utf-8"));
|
|
5260
|
+
return Array.isArray(parsed) && parsed.every((n) => typeof n === "number") ? parsed : null;
|
|
5261
|
+
} catch {
|
|
5262
|
+
return null;
|
|
5263
|
+
}
|
|
5264
|
+
}
|
|
5265
|
+
function registerPeakRoutes(api, adapter, decode = decodePeakMap) {
|
|
5266
|
+
api.get("/projects/:id/peaks/*", async (c) => {
|
|
5267
|
+
const project = await adapter.resolveProject(c.req.param("id"));
|
|
5268
|
+
if (!project) return c.json({ error: "not found" }, 404);
|
|
5269
|
+
const assetPath = requestSubPath(c.req.url, "projects/:id/peaks");
|
|
5270
|
+
const mediaPath = resolve6(project.dir, assetPath);
|
|
5271
|
+
if (!isWithinProjectRoot3(project.dir, mediaPath))
|
|
5272
|
+
return c.json({ error: "file not found" }, 404);
|
|
5273
|
+
const stats = statSync9(mediaPath, { throwIfNoEntry: false });
|
|
5274
|
+
if (!stats?.isFile()) return c.json({ error: "file not found" }, 404);
|
|
5275
|
+
const cacheDir = join14(project.dir, ".waveform-cache");
|
|
5276
|
+
const cachePath = join14(cacheDir, buildPeakMapCacheKey(assetPath, stats));
|
|
5277
|
+
const cached = readCachedBins(cacheDir, cachePath);
|
|
5278
|
+
if (cached) return c.json({ binSeconds: PEAK_BIN_SECONDS, bins: cached });
|
|
5279
|
+
let bins;
|
|
5280
|
+
try {
|
|
5281
|
+
bins = await decodeOnce(cachePath, mediaPath, decode);
|
|
5282
|
+
} catch {
|
|
5283
|
+
return c.json({ error: "failed to decode audio" }, 500);
|
|
5284
|
+
}
|
|
5285
|
+
try {
|
|
5286
|
+
writeWaveformCache(cachePath, bins);
|
|
5287
|
+
} catch {
|
|
5288
|
+
}
|
|
5289
|
+
return c.json({ binSeconds: PEAK_BIN_SECONDS, bins });
|
|
5290
|
+
});
|
|
5291
|
+
}
|
|
5292
|
+
|
|
4765
5293
|
// src/routes/fonts.ts
|
|
4766
5294
|
import { closeSync as closeSync3, constants, fstatSync as fstatSync3, openSync as openSync3, readSync } from "fs";
|
|
4767
5295
|
import {
|
|
@@ -4798,7 +5326,7 @@ var GOOGLE_FONT_FALLBACKS = [
|
|
|
4798
5326
|
"Outfit",
|
|
4799
5327
|
"JetBrains Mono"
|
|
4800
5328
|
];
|
|
4801
|
-
function
|
|
5329
|
+
function isRecord2(value) {
|
|
4802
5330
|
return typeof value === "object" && value !== null;
|
|
4803
5331
|
}
|
|
4804
5332
|
function collectFontsFromDir(dir) {
|
|
@@ -4822,10 +5350,10 @@ function listInstalledFontFamilies() {
|
|
|
4822
5350
|
return cachedFonts;
|
|
4823
5351
|
}
|
|
4824
5352
|
function parseGoogleFontMetadata(value) {
|
|
4825
|
-
if (!
|
|
5353
|
+
if (!isRecord2(value) || !Array.isArray(value.familyMetadataList)) return [];
|
|
4826
5354
|
const families = [];
|
|
4827
5355
|
for (const entry of value.familyMetadataList) {
|
|
4828
|
-
if (!
|
|
5356
|
+
if (!isRecord2(entry) || typeof entry.family !== "string") continue;
|
|
4829
5357
|
families.push(entry.family);
|
|
4830
5358
|
}
|
|
4831
5359
|
return families;
|
|
@@ -4929,14 +5457,14 @@ function registerRegistryRoutes(api, adapter) {
|
|
|
4929
5457
|
}
|
|
4930
5458
|
|
|
4931
5459
|
// src/routes/selection.ts
|
|
4932
|
-
function
|
|
5460
|
+
function isRecord3(value) {
|
|
4933
5461
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4934
5462
|
}
|
|
4935
5463
|
function isFiniteNumber(value) {
|
|
4936
5464
|
return typeof value === "number" && Number.isFinite(value);
|
|
4937
5465
|
}
|
|
4938
5466
|
function isStringRecord(value) {
|
|
4939
|
-
return
|
|
5467
|
+
return isRecord3(value) && Object.values(value).every((v) => typeof v === "string");
|
|
4940
5468
|
}
|
|
4941
5469
|
function hasString(value, key) {
|
|
4942
5470
|
return typeof value[key] === "string";
|
|
@@ -4954,20 +5482,20 @@ function hasOptionalNumber(value, key) {
|
|
|
4954
5482
|
return value[key] === void 0 || isFiniteNumber(value[key]);
|
|
4955
5483
|
}
|
|
4956
5484
|
function isBoundingBox(value) {
|
|
4957
|
-
return
|
|
5485
|
+
return isRecord3(value) && ["x", "y", "width", "height"].every((key) => isFiniteNumber(value[key]));
|
|
4958
5486
|
}
|
|
4959
5487
|
function isTarget(value) {
|
|
4960
|
-
if (!
|
|
5488
|
+
if (!isRecord3(value)) return false;
|
|
4961
5489
|
return hasOptionalNullableString(value, "id") && hasOptionalString(value, "hfId") && hasOptionalString(value, "selector") && hasOptionalNumber(value, "selectorIndex");
|
|
4962
5490
|
}
|
|
4963
5491
|
function isTextField(value) {
|
|
4964
|
-
return
|
|
5492
|
+
return isRecord3(value) && hasRequiredStrings(value, ["key", "label", "value", "tagName"]) && ["self", "child", "text-node"].includes(value.source);
|
|
4965
5493
|
}
|
|
4966
5494
|
function isTextFields(value) {
|
|
4967
5495
|
return Array.isArray(value) && value.every(isTextField);
|
|
4968
5496
|
}
|
|
4969
5497
|
function isSelectionSnapshot(value) {
|
|
4970
|
-
if (!
|
|
5498
|
+
if (!isRecord3(value)) return false;
|
|
4971
5499
|
const checks = [
|
|
4972
5500
|
value.schemaVersion === 1 && hasRequiredStrings(value, [
|
|
4973
5501
|
"projectId",
|
|
@@ -4985,7 +5513,7 @@ function isSelectionSnapshot(value) {
|
|
|
4985
5513
|
isStringRecord(value.inlineStyles),
|
|
4986
5514
|
isStringRecord(value.computedStyles),
|
|
4987
5515
|
isTextFields(value.textFields),
|
|
4988
|
-
|
|
5516
|
+
isRecord3(value.capabilities)
|
|
4989
5517
|
];
|
|
4990
5518
|
return checks.every(Boolean);
|
|
4991
5519
|
}
|
|
@@ -5009,7 +5537,7 @@ function registerSelectionRoutes(api, adapter) {
|
|
|
5009
5537
|
} catch {
|
|
5010
5538
|
return c.json({ error: "invalid json" }, 400);
|
|
5011
5539
|
}
|
|
5012
|
-
if (!
|
|
5540
|
+
if (!isRecord3(body) || !("selection" in body)) {
|
|
5013
5541
|
return c.json({ error: "missing selection" }, 400);
|
|
5014
5542
|
}
|
|
5015
5543
|
if (body.selection === null) {
|
|
@@ -5028,8 +5556,8 @@ function registerSelectionRoutes(api, adapter) {
|
|
|
5028
5556
|
|
|
5029
5557
|
// src/routes/media.ts
|
|
5030
5558
|
import { streamSSE as streamSSE2 } from "hono/streaming";
|
|
5031
|
-
import { existsSync as
|
|
5032
|
-
import { basename as basename2, dirname as
|
|
5559
|
+
import { existsSync as existsSync11 } from "fs";
|
|
5560
|
+
import { basename as basename2, dirname as dirname6, extname, join as join15, posix } from "path";
|
|
5033
5561
|
var VIDEO_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
5034
5562
|
".mp4",
|
|
5035
5563
|
".mov",
|
|
@@ -5066,7 +5594,7 @@ function uniqueAssetPath(projectDir, assetPath, taken) {
|
|
|
5066
5594
|
const ext = extname(assetPath);
|
|
5067
5595
|
const withoutExt = assetPath.slice(0, -ext.length);
|
|
5068
5596
|
let candidate = assetPath;
|
|
5069
|
-
for (let index = 2; taken.has(candidate) ||
|
|
5597
|
+
for (let index = 2; taken.has(candidate) || existsSync11(join15(projectDir, candidate)); index++) {
|
|
5070
5598
|
candidate = `${withoutExt}-${index}${ext}`;
|
|
5071
5599
|
}
|
|
5072
5600
|
return candidate;
|
|
@@ -5127,7 +5655,7 @@ function registerMediaRoutes(api, adapter, options = {}) {
|
|
|
5127
5655
|
}
|
|
5128
5656
|
const filePath = resolveWithinProject(project.dir, assetPath);
|
|
5129
5657
|
if (!filePath) return c.json({ error: "forbidden" }, 403);
|
|
5130
|
-
if (!
|
|
5658
|
+
if (!existsSync11(filePath)) return c.json({ error: "media not found" }, 404);
|
|
5131
5659
|
return c.json({ path: assetPath, metadata: await readMediaMetadata(filePath) });
|
|
5132
5660
|
});
|
|
5133
5661
|
api.post(
|
|
@@ -5149,7 +5677,7 @@ function registerMediaRoutes(api, adapter, options = {}) {
|
|
|
5149
5677
|
}
|
|
5150
5678
|
const inputPath = resolveWithinProject(project.dir, inputAssetPath);
|
|
5151
5679
|
if (!inputPath) return c.json({ error: "forbidden" }, 403);
|
|
5152
|
-
if (!
|
|
5680
|
+
if (!existsSync11(inputPath)) return c.json({ error: "input media not found" }, 404);
|
|
5153
5681
|
const inputIsVideo = isVideoPath(inputAssetPath);
|
|
5154
5682
|
const inputIsImage = isImagePath(inputAssetPath);
|
|
5155
5683
|
if (!inputIsVideo && !inputIsImage) {
|
|
@@ -5185,8 +5713,8 @@ function registerMediaRoutes(api, adapter, options = {}) {
|
|
|
5185
5713
|
return c.json({ error: "forbidden" }, 403);
|
|
5186
5714
|
}
|
|
5187
5715
|
}
|
|
5188
|
-
mkdirWithinProject(project.dir,
|
|
5189
|
-
if (backgroundOutputPath) mkdirWithinProject(project.dir,
|
|
5716
|
+
mkdirWithinProject(project.dir, dirname6(outputPath));
|
|
5717
|
+
if (backgroundOutputPath) mkdirWithinProject(project.dir, dirname6(backgroundOutputPath));
|
|
5190
5718
|
const jobId = makeJobId(project.id, mediaJobs);
|
|
5191
5719
|
const state = adapter.startBackgroundRemoval({
|
|
5192
5720
|
project,
|
|
@@ -5243,14 +5771,14 @@ function registerMediaRoutes(api, adapter, options = {}) {
|
|
|
5243
5771
|
}
|
|
5244
5772
|
|
|
5245
5773
|
// src/routes/globalAssets.ts
|
|
5246
|
-
import { existsSync as
|
|
5774
|
+
import { existsSync as existsSync12, readFileSync as readFileSync13 } from "fs";
|
|
5247
5775
|
import { homedir } from "os";
|
|
5248
|
-
import { join as
|
|
5776
|
+
import { join as join16 } from "path";
|
|
5249
5777
|
function readGlobalAssets(home = homedir()) {
|
|
5250
|
-
const manifestPath =
|
|
5251
|
-
if (!
|
|
5778
|
+
const manifestPath = join16(home, ".media", "manifest.jsonl");
|
|
5779
|
+
if (!existsSync12(manifestPath)) return [];
|
|
5252
5780
|
const out = [];
|
|
5253
|
-
for (const line of
|
|
5781
|
+
for (const line of readFileSync13(manifestPath, "utf8").split("\n")) {
|
|
5254
5782
|
if (!line.trim()) continue;
|
|
5255
5783
|
try {
|
|
5256
5784
|
const rec = JSON.parse(line);
|
|
@@ -5437,6 +5965,9 @@ function createStudioApi(adapter) {
|
|
|
5437
5965
|
registerSelectionRoutes(api, adapter);
|
|
5438
5966
|
registerMediaRoutes(api, adapter);
|
|
5439
5967
|
registerWaveformRoutes(api, adapter);
|
|
5968
|
+
registerFreezeFrameRoutes(api, adapter);
|
|
5969
|
+
registerLoudnessRoutes(api, adapter);
|
|
5970
|
+
registerPeakRoutes(api, adapter);
|
|
5440
5971
|
registerFontRoutes(api);
|
|
5441
5972
|
registerRegistryRoutes(api, adapter);
|
|
5442
5973
|
registerGlobalAssetRoutes(api);
|
|
@@ -5446,7 +5977,7 @@ function createStudioApi(adapter) {
|
|
|
5446
5977
|
|
|
5447
5978
|
// src/helpers/duplicateElement.ts
|
|
5448
5979
|
import { sameInstant } from "@hyperframes/core/clip-facts";
|
|
5449
|
-
import { ensureHfIds as
|
|
5980
|
+
import { ensureHfIds as ensureHfIds4 } from "@hyperframes/parsers/hf-ids";
|
|
5450
5981
|
function duplicateElementInHtml(source, target, newId, at) {
|
|
5451
5982
|
const { document, wrappedFragment } = parseSourceDocument(source);
|
|
5452
5983
|
const element = findTargetElement(document, target);
|
|
@@ -5456,23 +5987,23 @@ function duplicateElementInHtml(source, target, newId, at) {
|
|
|
5456
5987
|
if (duration === null || numericAttribute(element, "data-start") === null) {
|
|
5457
5988
|
return { html: source, matched: false, newId: null };
|
|
5458
5989
|
}
|
|
5459
|
-
const
|
|
5990
|
+
const uniqueId2 = nextUniqueId(document, newId);
|
|
5460
5991
|
rippleElements(document, element, at, duration, track);
|
|
5461
5992
|
const clone = element.cloneNode(true);
|
|
5462
5993
|
if (!isHTMLElement(clone)) return { html: source, matched: false, newId: null };
|
|
5463
|
-
clone.setAttribute("id",
|
|
5994
|
+
clone.setAttribute("id", uniqueId2);
|
|
5464
5995
|
dedupeClonedCompositionId(document, clone);
|
|
5465
5996
|
stripStableIds(clone);
|
|
5466
5997
|
clone.setAttribute("data-start", String(at));
|
|
5467
5998
|
element.parentElement.insertBefore(clone, element.nextSibling);
|
|
5468
5999
|
const html = wrappedFragment ? document.body.innerHTML || "" : document.toString();
|
|
5469
|
-
return { html:
|
|
6000
|
+
return { html: ensureHfIds4(html), matched: true, newId: uniqueId2 };
|
|
5470
6001
|
}
|
|
5471
6002
|
function nextUniqueId(document, newId) {
|
|
5472
|
-
let
|
|
6003
|
+
let uniqueId2 = newId;
|
|
5473
6004
|
let suffix = 2;
|
|
5474
|
-
while (document.getElementById(
|
|
5475
|
-
return
|
|
6005
|
+
while (document.getElementById(uniqueId2)) uniqueId2 = `${newId}-${suffix++}`;
|
|
6006
|
+
return uniqueId2;
|
|
5476
6007
|
}
|
|
5477
6008
|
function rippleElements(document, element, at, duration, track) {
|
|
5478
6009
|
for (const candidate of Array.from(document.querySelectorAll("[data-start][data-duration]"))) {
|
|
@@ -5498,18 +6029,18 @@ function numericAttribute(element, name) {
|
|
|
5498
6029
|
}
|
|
5499
6030
|
|
|
5500
6031
|
// src/helpers/previewDocumentStore.ts
|
|
5501
|
-
import { createHash as
|
|
5502
|
-
import { readFileSync as
|
|
5503
|
-
import { join as
|
|
5504
|
-
import { replaceFileAtomically as
|
|
6032
|
+
import { createHash as createHash5 } from "crypto";
|
|
6033
|
+
import { readFileSync as readFileSync14, readdirSync as readdirSync5, rmSync as rmSync5 } from "fs";
|
|
6034
|
+
import { join as join17 } from "path";
|
|
6035
|
+
import { replaceFileAtomically as replaceFileAtomically4 } from "@hyperframes/core/atomic-file";
|
|
5505
6036
|
function createPreviewDocumentStore(projectDir, salt) {
|
|
5506
|
-
const dir =
|
|
5507
|
-
const fileFor = (key) =>
|
|
6037
|
+
const dir = join17(projectDir, ".hyperframes", "preview");
|
|
6038
|
+
const fileFor = (key) => join17(dir, `${createHash5("sha256").update(`${salt}
|
|
5508
6039
|
${key}`).digest("hex").slice(0, 32)}.html`);
|
|
5509
6040
|
return {
|
|
5510
6041
|
read(key) {
|
|
5511
6042
|
try {
|
|
5512
|
-
return
|
|
6043
|
+
return readFileSync14(fileFor(key), "utf-8");
|
|
5513
6044
|
} catch {
|
|
5514
6045
|
return null;
|
|
5515
6046
|
}
|
|
@@ -5518,9 +6049,9 @@ ${key}`).digest("hex").slice(0, 32)}.html`);
|
|
|
5518
6049
|
try {
|
|
5519
6050
|
mkdirWithinProject(projectDir, dir);
|
|
5520
6051
|
const file = fileFor(key);
|
|
5521
|
-
|
|
6052
|
+
replaceFileAtomically4(file, html, 420);
|
|
5522
6053
|
for (const name of readdirSync5(dir)) {
|
|
5523
|
-
if (
|
|
6054
|
+
if (join17(dir, name) !== file) rmSync5(join17(dir, name), { force: true });
|
|
5524
6055
|
}
|
|
5525
6056
|
} catch {
|
|
5526
6057
|
}
|