@higgsfield/video-editor-mcp 1.1.0 → 2.0.0
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/README.md +63 -26
- package/dist/cli.mjs +284 -112
- package/dist/guide.md +2 -3
- package/dist/index.html +1 -1
- package/dist/launcher.mjs +34 -14
- package/dist/server.mjs +209 -265
- package/node_modules/@higgsfield/higgsedit/dist/api.d.ts +481 -0
- package/node_modules/@higgsfield/higgsedit/dist/{chunk-PFQYUJQL.mjs → chunk-3XVMJSV2.mjs} +1 -1
- package/node_modules/@higgsfield/higgsedit/dist/{chunk-F4CMIG5G.mjs → chunk-4VE54E4U.mjs} +3 -1
- package/node_modules/@higgsfield/higgsedit/dist/{chunk-2JYLVSVV.mjs → chunk-55JARHLA.mjs} +205 -65
- package/node_modules/@higgsfield/higgsedit/dist/{chunk-YLVXQIPX.mjs → chunk-E4N76T2R.mjs} +1 -1
- package/node_modules/@higgsfield/higgsedit/dist/{chunk-OCIZ36XD.mjs → chunk-ECRSOIKI.mjs} +1 -1
- package/node_modules/@higgsfield/higgsedit/dist/{chunk-IEPPEDEU.mjs → chunk-ENALCTAD.mjs} +1 -1
- package/node_modules/@higgsfield/higgsedit/dist/chunk-HY2E7IWX.mjs +43 -0
- package/node_modules/@higgsfield/higgsedit/dist/{chunk-NARTAWRN.mjs → chunk-HYCUOCUQ.mjs} +9 -4
- package/node_modules/@higgsfield/higgsedit/dist/{chunk-C3GSB3XZ.mjs → chunk-NLEIBCAK.mjs} +1 -7
- package/node_modules/@higgsfield/higgsedit/dist/{chunk-2WKNH7OL.mjs → chunk-O3AVYZ6U.mjs} +1 -1
- package/node_modules/@higgsfield/higgsedit/dist/chunk-UXO22E3P.mjs +22 -0
- package/node_modules/@higgsfield/higgsedit/dist/{chunk-BH2QF635.mjs → chunk-V5XTCFYK.mjs} +4 -100
- package/node_modules/@higgsfield/higgsedit/dist/chunk-W2TANYEH.mjs +118 -0
- package/node_modules/@higgsfield/higgsedit/dist/{chunk-DVN4LUXV.mjs → chunk-XY5CCUOZ.mjs} +2 -6
- package/node_modules/@higgsfield/higgsedit/dist/{chunk-KIWGVAPJ.mjs → chunk-Y7SAX2P4.mjs} +8 -12
- package/node_modules/@higgsfield/higgsedit/dist/chunk-YNAIRXOP.mjs +22 -0
- package/node_modules/@higgsfield/higgsedit/dist/{chunk-WENFTPXQ.mjs → chunk-ZO3NDQD3.mjs} +1 -1
- package/node_modules/@higgsfield/higgsedit/dist/higgsedit.mjs +13 -13
- package/node_modules/@higgsfield/higgsedit/package.json +4 -1
- package/package.json +3 -3
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/skills/video-editing/SKILL.md +109 -95
- package/plugin/mcp.json +0 -10
- package/plugin/plugin.json +0 -41
- package/plugin/skills/motion-craft/SKILL.md +0 -87
- package/plugin/skills/motion-craft/agents/openai.yaml +0 -6
- package/plugin/skills/motion-craft/references/causality.md +0 -25
- package/plugin/skills/motion-craft/references/composition.md +0 -20
- package/plugin/skills/motion-craft/references/easing.md +0 -19
- package/plugin/skills/motion-craft/references/qc.md +0 -33
- package/plugin/skills/motion-craft/references/recipes.md +0 -31
- package/plugin/skills/motion-craft/references/stepped.md +0 -43
- package/plugin/skills/motion-craft/references/timing.md +0 -60
- package/plugin/skills/motion-craft/references/tsx.md +0 -60
- package/plugin/skills/motion-craft/references/typography.md +0 -22
- package/plugin/skills/video-editing/agents/openai.yaml +0 -6
- package/plugin/skills/video-editing/references/animation-contract.md +0 -45
- package/plugin/skills/video-editing/references/assembly.md +0 -58
- package/plugin/skills/video-editing/references/caption-systems.md +0 -29
- package/plugin/skills/video-editing/references/caption-titling.md +0 -41
- package/plugin/skills/video-editing/references/clip-geometry.md +0 -37
- package/plugin/skills/video-editing/references/compose.md +0 -283
- package/plugin/skills/video-editing/references/editor-measured.md +0 -37
- package/plugin/skills/video-editing/references/failure-modes.md +0 -37
- package/plugin/skills/video-editing/references/local-setup.md +0 -25
- package/plugin/skills/video-editing/references/motion-language.md +0 -27
- package/plugin/skills/video-editing/references/provenance.md +0 -7
- package/plugin/skills/video-editing/references/sandbox.md +0 -31
- package/plugin/skills/video-editing/references/shot-blueprints.md +0 -24
- package/plugin/skills/video-editing/references/title-animation.md +0 -29
- package/plugin/skills/video-editing/references/tsx-projects.md +0 -187
- package/plugin/skills/video-editing/references/workflows.md +0 -41
|
@@ -7,18 +7,27 @@ const __dirname = __filename.slice(0, __filename.lastIndexOf("/"));
|
|
|
7
7
|
import {
|
|
8
8
|
api,
|
|
9
9
|
openCapture,
|
|
10
|
-
render
|
|
11
|
-
|
|
10
|
+
render,
|
|
11
|
+
sheetPng
|
|
12
|
+
} from "./chunk-HYCUOCUQ.mjs";
|
|
12
13
|
import {
|
|
13
14
|
addPreset,
|
|
14
15
|
presetCatalogue,
|
|
15
16
|
presetsDir,
|
|
16
17
|
readManifest
|
|
17
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-E4N76T2R.mjs";
|
|
19
|
+
import {
|
|
20
|
+
outline
|
|
21
|
+
} from "./chunk-NLEIBCAK.mjs";
|
|
22
|
+
import "./chunk-OF6RMSM2.mjs";
|
|
18
23
|
import {
|
|
19
24
|
analyze,
|
|
20
25
|
applyEdits
|
|
21
26
|
} from "./chunk-MV2JTTBQ.mjs";
|
|
27
|
+
import {
|
|
28
|
+
mediaSeconds,
|
|
29
|
+
segment
|
|
30
|
+
} from "./chunk-Y7SAX2P4.mjs";
|
|
22
31
|
import {
|
|
23
32
|
MEDIA_UPLOAD_TEMP_PREFIX,
|
|
24
33
|
MediaUploads,
|
|
@@ -39,6 +48,9 @@ import {
|
|
|
39
48
|
rel,
|
|
40
49
|
sourceFiles
|
|
41
50
|
} from "./chunk-LTKMGZ6T.mjs";
|
|
51
|
+
import {
|
|
52
|
+
diagnose
|
|
53
|
+
} from "./chunk-XY5CCUOZ.mjs";
|
|
42
54
|
import {
|
|
43
55
|
CHROME_VERSION
|
|
44
56
|
} from "./chunk-P2BGHTRR.mjs";
|
|
@@ -48,13 +60,25 @@ import {
|
|
|
48
60
|
requireSelectBackend,
|
|
49
61
|
select,
|
|
50
62
|
track
|
|
51
|
-
} from "./chunk-
|
|
52
|
-
import
|
|
63
|
+
} from "./chunk-ZO3NDQD3.mjs";
|
|
64
|
+
import {
|
|
65
|
+
filmstrip,
|
|
66
|
+
grab,
|
|
67
|
+
transcribeMedia,
|
|
68
|
+
waveform
|
|
69
|
+
} from "./chunk-V5XTCFYK.mjs";
|
|
70
|
+
import {
|
|
71
|
+
probeMedia,
|
|
72
|
+
resolveMediaPath
|
|
73
|
+
} from "./chunk-QJTILFBS.mjs";
|
|
74
|
+
import "./chunk-L5LWBA6A.mjs";
|
|
53
75
|
import {
|
|
54
76
|
ffmpeg,
|
|
55
77
|
requireTool
|
|
56
78
|
} from "./chunk-PAHMD6SU.mjs";
|
|
57
|
-
import
|
|
79
|
+
import {
|
|
80
|
+
listFonts
|
|
81
|
+
} from "./chunk-NUXTKQNM.mjs";
|
|
58
82
|
import "./chunk-2VKQHJMM.mjs";
|
|
59
83
|
import "./chunk-BV5COOQZ.mjs";
|
|
60
84
|
import {
|
|
@@ -65,28 +89,142 @@ import "./chunk-75WO2NOF.mjs";
|
|
|
65
89
|
|
|
66
90
|
// src/dev.ts
|
|
67
91
|
import { existsSync as existsSync4, linkSync, lstatSync as lstatSync2, mkdirSync as mkdirSync4, mkdtempSync, readdirSync as readdirSync3, readFileSync as readFileSync4, renameSync as renameSync4, rmSync as rmSync3, statSync as statSync4, watch } from "node:fs";
|
|
68
|
-
import { basename, extname, join as
|
|
92
|
+
import { basename as basename2, extname as extname2, join as join6 } from "node:path";
|
|
93
|
+
|
|
94
|
+
// src/api-routes.ts
|
|
95
|
+
import { basename, extname, join } from "node:path";
|
|
96
|
+
var Jobs = class {
|
|
97
|
+
jobs = /* @__PURE__ */ new Map();
|
|
98
|
+
seq = 0;
|
|
99
|
+
start(run2) {
|
|
100
|
+
const id = String(++this.seq);
|
|
101
|
+
this.jobs.set(id, { status: "running" });
|
|
102
|
+
run2().then(
|
|
103
|
+
(result) => this.jobs.set(id, { status: "done", result }),
|
|
104
|
+
(error) => this.jobs.set(id, { status: "failed", error: error instanceof Error ? error.message : String(error) })
|
|
105
|
+
);
|
|
106
|
+
return { id };
|
|
107
|
+
}
|
|
108
|
+
state(id) {
|
|
109
|
+
return this.jobs.get(id);
|
|
110
|
+
}
|
|
111
|
+
busy() {
|
|
112
|
+
return [...this.jobs.values()].some((job) => job.status === "running");
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
var string = (value, what) => {
|
|
116
|
+
if (typeof value !== "string" || !value) throw new Error(`${what} must be a non-empty string`);
|
|
117
|
+
return value;
|
|
118
|
+
};
|
|
119
|
+
var route = (method, path, run2) => async (req, res, url) => {
|
|
120
|
+
if (url.pathname !== path || req.method !== method) return false;
|
|
121
|
+
let body = {};
|
|
122
|
+
if (method === "POST") {
|
|
123
|
+
try {
|
|
124
|
+
body = JSON.parse((await readBody(req)).toString() || "{}");
|
|
125
|
+
} catch {
|
|
126
|
+
json(res, 400, { error: "the body must be JSON" });
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) {
|
|
131
|
+
json(res, 400, { error: "the body must be a JSON object" });
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
json(res, 200, await run2(body, url));
|
|
135
|
+
return true;
|
|
136
|
+
};
|
|
137
|
+
function apiRoutes(dir, jobs) {
|
|
138
|
+
const media = (body) => resolveMediaPath(string(body.path, "path"), dir);
|
|
139
|
+
const output = (value) => typeof value === "string" && value ? resolveMediaPath(value, dir) : void 0;
|
|
140
|
+
const withOutput = (options) => ({ ...options, output: output(options.output) });
|
|
141
|
+
return [
|
|
142
|
+
route("GET", "/api/outline", () => outline(dir)),
|
|
143
|
+
async (req, res, url) => {
|
|
144
|
+
if (url.pathname !== "/api/sheet" || req.method !== "POST") return false;
|
|
145
|
+
const body = JSON.parse((await readBody(req)).toString() || "{}");
|
|
146
|
+
if (!Array.isArray(body.times) || !body.times.length) throw new Error("times must list at least one time");
|
|
147
|
+
const times = body.times.map(String);
|
|
148
|
+
const { png } = await sheetPng(dir, times, body.columns ?? Math.min(4, times.length), body.width ?? 480, body.allowFontFallback === true);
|
|
149
|
+
res.setHeader("content-type", "image/png");
|
|
150
|
+
res.end(png);
|
|
151
|
+
return true;
|
|
152
|
+
},
|
|
153
|
+
route("POST", "/api/media/probe", (body) => probeMedia(media(body))),
|
|
154
|
+
route("POST", "/api/media/grab", (body) => grab(media(body), withOutput(body))),
|
|
155
|
+
route("POST", "/api/media/filmstrip", (body) => filmstrip(media(body), withOutput(body))),
|
|
156
|
+
route("POST", "/api/media/waveform", (body) => waveform(media(body), withOutput(body))),
|
|
157
|
+
route("POST", "/api/media/transcribe", async (body) => {
|
|
158
|
+
const file = media(body);
|
|
159
|
+
return jobs.start(() => transcribeMedia(file, withOutput(body)));
|
|
160
|
+
}),
|
|
161
|
+
route("POST", "/api/media/segment", async (body) => {
|
|
162
|
+
const file = media(body);
|
|
163
|
+
const request = body;
|
|
164
|
+
const fps = await mediaSeconds.fps(file);
|
|
165
|
+
const time = (value, what) => mediaSeconds.parse(value === void 0 ? void 0 : String(value), fps, what);
|
|
166
|
+
const at = time(request.at, "at");
|
|
167
|
+
const start = time(request.start, "start");
|
|
168
|
+
const end = time(request.end, "end");
|
|
169
|
+
const out = output(request.output) ?? join(dir, "masks", `${basename(file, extname(file))}.mask`);
|
|
170
|
+
return jobs.start(
|
|
171
|
+
() => segment(file, {
|
|
172
|
+
...request.points ? { points: request.points } : {},
|
|
173
|
+
...request.exclude ? { exclude: request.exclude } : {},
|
|
174
|
+
...request.box ? { box: request.box } : {},
|
|
175
|
+
...at !== void 0 ? { at } : {},
|
|
176
|
+
...start !== void 0 ? { start } : {},
|
|
177
|
+
...end !== void 0 ? { end } : {},
|
|
178
|
+
...request.model ? { model: request.model } : {},
|
|
179
|
+
out
|
|
180
|
+
})
|
|
181
|
+
);
|
|
182
|
+
}),
|
|
183
|
+
(req, res, url) => {
|
|
184
|
+
const id = /^\/api\/jobs\/([^/]+)$/.exec(url.pathname)?.[1];
|
|
185
|
+
if (id === void 0 || req.method !== "GET") return false;
|
|
186
|
+
const job = jobs.state(decodeURIComponent(id));
|
|
187
|
+
if (job) json(res, 200, job);
|
|
188
|
+
else json(res, 404, { error: `no job ${decodeURIComponent(id)}` });
|
|
189
|
+
return true;
|
|
190
|
+
},
|
|
191
|
+
route("GET", "/api/fonts", (_body, url) => {
|
|
192
|
+
const q = url.searchParams;
|
|
193
|
+
const category = q.get("category");
|
|
194
|
+
const limit = q.get("limit");
|
|
195
|
+
const offset = q.get("offset");
|
|
196
|
+
return listFonts({
|
|
197
|
+
...q.get("family") ? { family: q.get("family") } : {},
|
|
198
|
+
...category ? { category } : {},
|
|
199
|
+
...q.get("cached") === "1" || q.get("cached") === "true" ? { cached: true } : {},
|
|
200
|
+
...limit ? { limit: Number(limit) } : {},
|
|
201
|
+
...offset ? { offset: Number(offset) } : {}
|
|
202
|
+
});
|
|
203
|
+
}),
|
|
204
|
+
route("GET", "/api/doctor", () => diagnose())
|
|
205
|
+
];
|
|
206
|
+
}
|
|
69
207
|
|
|
70
208
|
// src/editor-build.ts
|
|
71
209
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
72
|
-
import { join } from "node:path";
|
|
73
|
-
var EDITOR =
|
|
74
|
-
var EDITOR_DIST = PACKAGED ?
|
|
210
|
+
import { join as join2 } from "node:path";
|
|
211
|
+
var EDITOR = join2(import.meta.dirname, "../../editor");
|
|
212
|
+
var EDITOR_DIST = PACKAGED ? join2(import.meta.dirname, "editor") : join2(EDITOR, "dist");
|
|
75
213
|
var EDITOR_MANIFEST = "editor/.vite/manifest.json";
|
|
76
|
-
var MANIFEST =
|
|
214
|
+
var MANIFEST = join2(EDITOR_DIST, ".vite/manifest.json");
|
|
77
215
|
function newest(dir) {
|
|
78
216
|
let t = 0;
|
|
79
217
|
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
80
|
-
const p =
|
|
218
|
+
const p = join2(dir, e.name);
|
|
81
219
|
t = Math.max(t, e.isDirectory() ? newest(p) : statSync(p).mtimeMs);
|
|
82
220
|
}
|
|
83
221
|
return t;
|
|
84
222
|
}
|
|
85
223
|
var PACKAGES = ["editor-document", "editor-kernel", "encoder", "jsx", "reconciler", "runtime", "writeback", "ui-kit", "mcp-app", "mcp-ui-resources"];
|
|
86
|
-
var inputs = () => [
|
|
224
|
+
var inputs = () => [join2(EDITOR, "src"), ...PACKAGES.map((p) => join2(EDITOR, `../../packages/${p}/src`))].filter((dir) => existsSync(dir));
|
|
87
225
|
var MissingEditorError = class extends Error {
|
|
88
226
|
constructor() {
|
|
89
|
-
super("this higgsedit build has no editor page: install
|
|
227
|
+
super("this higgsedit build has no editor page: install it from npm, or run `higgsedit dev` from a checkout");
|
|
90
228
|
}
|
|
91
229
|
};
|
|
92
230
|
var building;
|
|
@@ -106,7 +244,7 @@ async function editorEntry(log = () => {
|
|
|
106
244
|
const env = process.env.NODE_ENV;
|
|
107
245
|
process.env.NODE_ENV = "production";
|
|
108
246
|
try {
|
|
109
|
-
await build({ configFile:
|
|
247
|
+
await build({ configFile: join2(EDITOR, "vite.config.ts"), logLevel: "error", mode: "production" });
|
|
110
248
|
} finally {
|
|
111
249
|
process.env.NODE_ENV = env;
|
|
112
250
|
}
|
|
@@ -118,7 +256,7 @@ async function editorEntry(log = () => {
|
|
|
118
256
|
}
|
|
119
257
|
return manifestEntry(readFileSync(MANIFEST, "utf8"));
|
|
120
258
|
}
|
|
121
|
-
var editorFile = (rel2) => PACKAGED ? packagedAsset(`editor/${rel2}`) :
|
|
259
|
+
var editorFile = (rel2) => PACKAGED ? packagedAsset(`editor/${rel2}`) : join2(EDITOR_DIST, rel2);
|
|
122
260
|
function manifestEntry(text) {
|
|
123
261
|
const manifest = JSON.parse(text);
|
|
124
262
|
const entry = Object.values(manifest).find((m) => m.isEntry);
|
|
@@ -245,7 +383,7 @@ function validateRenderRequest(input, supported) {
|
|
|
245
383
|
// src/history.ts
|
|
246
384
|
import { randomUUID } from "node:crypto";
|
|
247
385
|
import { closeSync, constants, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync as readFileSync2, readSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
248
|
-
import { join as
|
|
386
|
+
import { join as join3 } from "node:path";
|
|
249
387
|
var MAX_BYTES = 16 * 1024 * 1024;
|
|
250
388
|
var MAX_ENTRIES = 100;
|
|
251
389
|
var STALE = "edit history changed in another editor; reopen the project before editing or undoing";
|
|
@@ -407,7 +545,7 @@ var EditHistory = class {
|
|
|
407
545
|
if (!safePath(path)) throw new Error("unsafe edit history source path");
|
|
408
546
|
let target = this.dir;
|
|
409
547
|
for (const part of path.split("/")) {
|
|
410
|
-
target =
|
|
548
|
+
target = join3(target, part);
|
|
411
549
|
try {
|
|
412
550
|
if (lstatSync(target).isSymbolicLink()) throw new Error(`${path} is a symlink; cannot restore edit history`);
|
|
413
551
|
} catch (error) {
|
|
@@ -418,12 +556,12 @@ var EditHistory = class {
|
|
|
418
556
|
return readFileSync2(target, "utf8");
|
|
419
557
|
}
|
|
420
558
|
write(path, text) {
|
|
421
|
-
const target =
|
|
559
|
+
const target = join3(this.dir, path);
|
|
422
560
|
this.onWrite?.(path, text);
|
|
423
561
|
this.atomicWrite(target, text, lstatSync(target).mode & 511);
|
|
424
562
|
}
|
|
425
563
|
cacheDir() {
|
|
426
|
-
const cache =
|
|
564
|
+
const cache = join3(this.dir, ".higgsedit-cache");
|
|
427
565
|
try {
|
|
428
566
|
const stat = lstatSync(cache);
|
|
429
567
|
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("unsafe edit history cache directory");
|
|
@@ -433,7 +571,7 @@ var EditHistory = class {
|
|
|
433
571
|
return cache;
|
|
434
572
|
}
|
|
435
573
|
readJournal() {
|
|
436
|
-
const file =
|
|
574
|
+
const file = join3(this.cacheDir(), "history.json");
|
|
437
575
|
let fd;
|
|
438
576
|
try {
|
|
439
577
|
if (!lstatSync(file).isFile()) throw new Error("edit history journal is not a regular file");
|
|
@@ -463,7 +601,7 @@ var EditHistory = class {
|
|
|
463
601
|
assertCurrent() {
|
|
464
602
|
let revision;
|
|
465
603
|
try {
|
|
466
|
-
const stat = lstatSync(
|
|
604
|
+
const stat = lstatSync(join3(this.cacheDir(), "history.json"), { bigint: true });
|
|
467
605
|
if (!stat.isFile()) throw new Error("edit history journal is not a regular file");
|
|
468
606
|
revision = identity(stat);
|
|
469
607
|
} catch (error) {
|
|
@@ -501,7 +639,7 @@ var EditHistory = class {
|
|
|
501
639
|
this.assertCurrent();
|
|
502
640
|
const cache = this.cacheDir();
|
|
503
641
|
mkdirSync(cache, { recursive: true, mode: 448 });
|
|
504
|
-
const target =
|
|
642
|
+
const target = join3(cache, "history.json");
|
|
505
643
|
try {
|
|
506
644
|
this.revision = this.atomicWrite(target, next.text, 384);
|
|
507
645
|
} catch (error) {
|
|
@@ -540,7 +678,7 @@ var EditHistory = class {
|
|
|
540
678
|
import { createHash } from "node:crypto";
|
|
541
679
|
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, readdirSync as readdirSync2, renameSync as renameSync2, rmSync, statSync as statSync2, utimesSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
542
680
|
import { once } from "node:events";
|
|
543
|
-
import { join as
|
|
681
|
+
import { join as join4, posix } from "node:path";
|
|
544
682
|
var PRERENDER_DIR = ".higgsedit/prerender";
|
|
545
683
|
var DEFAULT_CAP_MB = 1024;
|
|
546
684
|
var GOP_SECONDS = 0.5;
|
|
@@ -560,7 +698,7 @@ var Prerenderer = class {
|
|
|
560
698
|
this.dir = dir;
|
|
561
699
|
this.onEvent = onEvent;
|
|
562
700
|
this.log = log;
|
|
563
|
-
this.root =
|
|
701
|
+
this.root = join4(dir, PRERENDER_DIR);
|
|
564
702
|
this.capBytes = (opts.capMb ?? Number(process.env.HIGGSEDIT_PRERENDER_MB ?? DEFAULT_CAP_MB)) * 1024 * 1024;
|
|
565
703
|
}
|
|
566
704
|
dir;
|
|
@@ -595,7 +733,7 @@ ${code}`));
|
|
|
595
733
|
const files = rel2.includes("*") ? this.expand(rel2) : [rel2];
|
|
596
734
|
for (const f of files) {
|
|
597
735
|
try {
|
|
598
|
-
const st = statSync2(
|
|
736
|
+
const st = statSync2(join4(this.dir, f));
|
|
599
737
|
if (st.isFile()) lines.push(`${f} ${st.size} ${st.mtimeMs}`);
|
|
600
738
|
} catch {
|
|
601
739
|
lines.push(`${f} -`);
|
|
@@ -603,7 +741,7 @@ ${code}`));
|
|
|
603
741
|
}
|
|
604
742
|
}
|
|
605
743
|
for (const f of fontFiles(this.dir)) {
|
|
606
|
-
const st = statSync2(
|
|
744
|
+
const st = statSync2(join4(this.dir, f));
|
|
607
745
|
lines.push(`font ${f} ${st.size} ${st.mtimeMs}`);
|
|
608
746
|
}
|
|
609
747
|
return lines.join("\n");
|
|
@@ -613,7 +751,7 @@ ${code}`));
|
|
|
613
751
|
if (dir.includes("*")) return [];
|
|
614
752
|
const re = glob(posix.basename(pattern));
|
|
615
753
|
try {
|
|
616
|
-
return readdirSync2(
|
|
754
|
+
return readdirSync2(join4(this.dir, dir)).filter((n) => re.test(n)).map((n) => posix.join(dir, n)).toSorted();
|
|
617
755
|
} catch {
|
|
618
756
|
return [];
|
|
619
757
|
}
|
|
@@ -627,7 +765,7 @@ ${target.size}`).slice(0, 32);
|
|
|
627
765
|
}
|
|
628
766
|
entry(id) {
|
|
629
767
|
try {
|
|
630
|
-
return JSON.parse(readFileSync3(
|
|
768
|
+
return JSON.parse(readFileSync3(join4(this.root, `${id}.json`), "utf8"));
|
|
631
769
|
} catch {
|
|
632
770
|
return void 0;
|
|
633
771
|
}
|
|
@@ -635,9 +773,9 @@ ${target.size}`).slice(0, 32);
|
|
|
635
773
|
/** The cache file `name` (`<id>.mp4`, …) if it exists; marks it used. */
|
|
636
774
|
lookup(name) {
|
|
637
775
|
if (!/^[0-9a-f]{32}(\.matte)?\.(mp4|json)$/.test(name)) return void 0;
|
|
638
|
-
const file =
|
|
776
|
+
const file = join4(this.root, name);
|
|
639
777
|
if (!existsSync2(file)) return void 0;
|
|
640
|
-
const meta =
|
|
778
|
+
const meta = join4(this.root, `${name.slice(0, 32)}.json`);
|
|
641
779
|
const now = /* @__PURE__ */ new Date();
|
|
642
780
|
try {
|
|
643
781
|
utimesSync(meta, now, now);
|
|
@@ -757,10 +895,10 @@ ${target.size}`).slice(0, 32);
|
|
|
757
895
|
const height = even(job.size);
|
|
758
896
|
const width = even(input.width * height / input.height);
|
|
759
897
|
mkdirSync2(this.root, { recursive: true });
|
|
760
|
-
const ignore =
|
|
898
|
+
const ignore = join4(this.root, "..", ".gitignore");
|
|
761
899
|
if (!existsSync2(ignore)) writeFileSync2(ignore, "*\n");
|
|
762
|
-
const colour =
|
|
763
|
-
const matte =
|
|
900
|
+
const colour = join4(this.root, `${id}.part.mp4`);
|
|
901
|
+
const matte = join4(this.root, `${id}.matte.part.mp4`);
|
|
764
902
|
const gop = String(Math.max(1, Math.round(input.fps * GOP_SECONDS)));
|
|
765
903
|
const tags = ["-colorspace", "bt709", "-color_primaries", "bt709", "-color_trc", "bt709"];
|
|
766
904
|
const encoder = ffmpeg(
|
|
@@ -865,13 +1003,13 @@ ${target.size}`).slice(0, 32);
|
|
|
865
1003
|
rmSync(colour, { force: true });
|
|
866
1004
|
rmSync(matte, { force: true });
|
|
867
1005
|
} else {
|
|
868
|
-
renameSync2(colour,
|
|
1006
|
+
renameSync2(colour, join4(this.root, `${id}.mp4`));
|
|
869
1007
|
if (opaque) rmSync(matte, { force: true });
|
|
870
|
-
else renameSync2(matte,
|
|
871
|
-
entry.bytes = statSync2(
|
|
1008
|
+
else renameSync2(matte, join4(this.root, `${id}.matte.mp4`));
|
|
1009
|
+
entry.bytes = statSync2(join4(this.root, `${id}.mp4`)).size + (opaque ? 0 : statSync2(join4(this.root, `${id}.matte.mp4`)).size);
|
|
872
1010
|
}
|
|
873
|
-
writeFileSync2(
|
|
874
|
-
renameSync2(
|
|
1011
|
+
writeFileSync2(join4(this.root, `${id}.json.part`), JSON.stringify(entry));
|
|
1012
|
+
renameSync2(join4(this.root, `${id}.json.part`), join4(this.root, `${id}.json`));
|
|
875
1013
|
return entry;
|
|
876
1014
|
}
|
|
877
1015
|
/** Drops the least recently used pictures until the folder fits its cap; never one the editor asked for last. */
|
|
@@ -887,7 +1025,7 @@ ${target.size}`).slice(0, 32);
|
|
|
887
1025
|
for (const n of names) {
|
|
888
1026
|
const id = n.slice(0, 32);
|
|
889
1027
|
if (!/^[0-9a-f]{32}/.test(id) || n.includes(".part")) continue;
|
|
890
|
-
const st = statSync2(
|
|
1028
|
+
const st = statSync2(join4(this.root, n));
|
|
891
1029
|
const g = groups.get(id) ?? { size: 0, used: 0 };
|
|
892
1030
|
g.size += st.size;
|
|
893
1031
|
if (n.endsWith(".json")) g.used = st.mtimeMs;
|
|
@@ -897,7 +1035,7 @@ ${target.size}`).slice(0, 32);
|
|
|
897
1035
|
for (const [id, g] of [...groups].toSorted((a, b) => a[1].used - b[1].used)) {
|
|
898
1036
|
if (total <= this.capBytes) break;
|
|
899
1037
|
if (wanted.has(id)) continue;
|
|
900
|
-
for (const ext of [".json", ".mp4", ".matte.mp4"]) rmSync(
|
|
1038
|
+
for (const ext of [".json", ".mp4", ".matte.mp4"]) rmSync(join4(this.root, `${id}${ext}`), { force: true });
|
|
901
1039
|
total -= g.size;
|
|
902
1040
|
}
|
|
903
1041
|
}
|
|
@@ -921,7 +1059,7 @@ var pageApi = (page) => ({
|
|
|
921
1059
|
import { execFile } from "node:child_process";
|
|
922
1060
|
import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
|
|
923
1061
|
import { existsSync as existsSync3, mkdirSync as mkdirSync3, renameSync as renameSync3, rmSync as rmSync2, statSync as statSync3 } from "node:fs";
|
|
924
|
-
import { join as
|
|
1062
|
+
import { join as join5 } from "node:path";
|
|
925
1063
|
import { promisify } from "node:util";
|
|
926
1064
|
var run = promisify(execFile);
|
|
927
1065
|
var VIDEO = /\.(mp4|mov|m4v|mkv|webm)$/i;
|
|
@@ -938,7 +1076,7 @@ var Proxies = class {
|
|
|
938
1076
|
this.dir = dir;
|
|
939
1077
|
this.onReady = onReady;
|
|
940
1078
|
this.log = log;
|
|
941
|
-
this.root =
|
|
1079
|
+
this.root = join5(dir, ".higgsedit-cache", "proxies");
|
|
942
1080
|
}
|
|
943
1081
|
dir;
|
|
944
1082
|
onReady;
|
|
@@ -953,7 +1091,7 @@ var Proxies = class {
|
|
|
953
1091
|
stopped = false;
|
|
954
1092
|
version(rel2) {
|
|
955
1093
|
try {
|
|
956
|
-
const st = statSync3(
|
|
1094
|
+
const st = statSync3(join5(this.dir, rel2));
|
|
957
1095
|
if (!st.isFile()) return void 0;
|
|
958
1096
|
return { key: createHash2("sha1").update(`${rel2}\0${st.size}\0${st.mtimeMs}\0${st.ctimeMs}\0${st.ino}`).digest("hex").slice(0, 20), mtime: st.mtimeMs };
|
|
959
1097
|
} catch {
|
|
@@ -963,7 +1101,7 @@ var Proxies = class {
|
|
|
963
1101
|
/** The finished proxy for a project-relative video path, if there is one. */
|
|
964
1102
|
lookup(rel2) {
|
|
965
1103
|
const key = this.version(rel2)?.key;
|
|
966
|
-
const file = key &&
|
|
1104
|
+
const file = key && join5(this.root, `${key}.mp4`);
|
|
967
1105
|
return file && existsSync3(file) ? file : void 0;
|
|
968
1106
|
}
|
|
969
1107
|
/** Only /preview/ demand starts work; watcher events refresh previously requested footage. */
|
|
@@ -977,7 +1115,7 @@ var Proxies = class {
|
|
|
977
1115
|
return;
|
|
978
1116
|
}
|
|
979
1117
|
const now = Date.now();
|
|
980
|
-
const candidate = prior?.key === version.key ? prior : { ...version, readyAt: now + Math.min(QUIET_MS, Math.max(0, QUIET_MS - (now - version.mtime))), done: existsSync3(
|
|
1118
|
+
const candidate = prior?.key === version.key ? prior : { ...version, readyAt: now + Math.min(QUIET_MS, Math.max(0, QUIET_MS - (now - version.mtime))), done: existsSync3(join5(this.root, `${version.key}.mp4`)) };
|
|
981
1119
|
if (requested && candidate.retryAt !== void 0 && now >= candidate.retryAt) {
|
|
982
1120
|
candidate.done = false;
|
|
983
1121
|
candidate.retryAt = void 0;
|
|
@@ -1033,7 +1171,7 @@ var Proxies = class {
|
|
|
1033
1171
|
if (Number.isFinite(next)) this.timer = setTimeout(() => this.schedule(), Math.max(0, next - Date.now()));
|
|
1034
1172
|
}
|
|
1035
1173
|
async make(rel2, key) {
|
|
1036
|
-
const file =
|
|
1174
|
+
const file = join5(this.dir, rel2);
|
|
1037
1175
|
if (this.lookup(rel2)) return;
|
|
1038
1176
|
const { stdout } = await run(requireTool("ffprobe"), ["-v", "error", "-select_streams", "v:0", "-show_entries", "stream=width,height,codec_name,time_base", "-of", "json", file], { maxBuffer: 1 << 20, timeout: 3e4, signal: this.abort.signal });
|
|
1039
1177
|
if (this.stopped || this.version(rel2)?.key !== key) return;
|
|
@@ -1046,8 +1184,8 @@ var Proxies = class {
|
|
|
1046
1184
|
const scale = LONG_SIDE / Math.max(facts.width, facts.height);
|
|
1047
1185
|
const [w, h] = scale < 1 ? [even2(facts.width * scale), even2(facts.height * scale)] : [even2(facts.width), even2(facts.height)];
|
|
1048
1186
|
mkdirSync3(this.root, { recursive: true });
|
|
1049
|
-
const out =
|
|
1050
|
-
const part =
|
|
1187
|
+
const out = join5(this.root, `${key}.mp4`);
|
|
1188
|
+
const part = join5(this.root, `${key}.${randomUUID2()}.part.mp4`);
|
|
1051
1189
|
const t0 = performance.now();
|
|
1052
1190
|
this.log(`making a preview proxy for ${rel2} (${facts.width}\xD7${facts.height} \u2192 ${w}\xD7${h})\u2026`);
|
|
1053
1191
|
const args = ["-y", "-i", file, "-map", "0:v:0", "-map", "0:a:0?", "-vf", `scale=${w}:${h}`, "-c:v", "libx264", "-preset", "veryfast", "-crf", "20", "-bf", "0", "-g", "12", "-pix_fmt", "yuv420p"];
|
|
@@ -1125,7 +1263,7 @@ function selectRequest(dir, raw) {
|
|
|
1125
1263
|
const end = seconds(body.end, "end");
|
|
1126
1264
|
return {
|
|
1127
1265
|
file,
|
|
1128
|
-
stem:
|
|
1266
|
+
stem: basename2(file, extname2(file)),
|
|
1129
1267
|
time: seconds(body.time, "time") ?? 0,
|
|
1130
1268
|
prompt: parsePrompt(body.prompt),
|
|
1131
1269
|
...Array.isArray(pick) ? { pick } : {},
|
|
@@ -1138,7 +1276,7 @@ async function startDev(dir, port = 0, options = {}) {
|
|
|
1138
1276
|
const mediaUploads = new MediaUploads(dir, options.mediaUploads);
|
|
1139
1277
|
const clients = /* @__PURE__ */ new Set();
|
|
1140
1278
|
const known = /* @__PURE__ */ new Map();
|
|
1141
|
-
for (const f of sourceFiles(dir)) known.set(f, readFileSync4(
|
|
1279
|
+
for (const f of sourceFiles(dir)) known.set(f, readFileSync4(join6(dir, f), "utf8"));
|
|
1142
1280
|
const ownWrites = /* @__PURE__ */ new Map();
|
|
1143
1281
|
const history = new EditHistory(dir, (path, text) => ownWrites.set(path, text));
|
|
1144
1282
|
const broadcast = (event) => {
|
|
@@ -1163,8 +1301,8 @@ async function startDev(dir, port = 0, options = {}) {
|
|
|
1163
1301
|
for (const f of /* @__PURE__ */ new Set([...present, ...known.keys()])) {
|
|
1164
1302
|
let text;
|
|
1165
1303
|
try {
|
|
1166
|
-
text = readFileSync4(
|
|
1167
|
-
mtime = Math.max(mtime, statSync4(
|
|
1304
|
+
text = readFileSync4(join6(dir, f), "utf8");
|
|
1305
|
+
mtime = Math.max(mtime, statSync4(join6(dir, f)).mtimeMs);
|
|
1168
1306
|
} catch {
|
|
1169
1307
|
text = void 0;
|
|
1170
1308
|
}
|
|
@@ -1193,10 +1331,11 @@ async function startDev(dir, port = 0, options = {}) {
|
|
|
1193
1331
|
const jobs = /* @__PURE__ */ new Map();
|
|
1194
1332
|
let jobSeq = 0;
|
|
1195
1333
|
const tracks = /* @__PURE__ */ new Map();
|
|
1334
|
+
const apiJobs = new Jobs();
|
|
1196
1335
|
let stopping = false;
|
|
1197
1336
|
let mutations = 0;
|
|
1198
1337
|
let stopped;
|
|
1199
|
-
const busy = () => mediaUploads.busy || mutations > 0 || [...jobs.values()].some((job) => job.state === "running") || [...tracks.values()].some((job) => job.state.status === "running");
|
|
1338
|
+
const busy = () => mediaUploads.busy || mutations > 0 || [...jobs.values()].some((job) => job.state === "running") || [...tracks.values()].some((job) => job.state.status === "running") || apiJobs.busy();
|
|
1200
1339
|
const server = await startServer(
|
|
1201
1340
|
dir,
|
|
1202
1341
|
[
|
|
@@ -1327,16 +1466,16 @@ async function startDev(dir, port = 0, options = {}) {
|
|
|
1327
1466
|
}
|
|
1328
1467
|
const requested = (body.fileName ?? "render.mp4").replace(/[/\\]/g, "_");
|
|
1329
1468
|
const name = requested.endsWith(".mp4") ? requested : `${requested}.mp4`;
|
|
1330
|
-
const renders =
|
|
1469
|
+
const renders = join6(dir, "renders");
|
|
1331
1470
|
mkdirSync4(renders, { recursive: true });
|
|
1332
1471
|
const directory = lstatSync2(renders);
|
|
1333
1472
|
if (!directory.isDirectory() || directory.isSymbolicLink()) {
|
|
1334
1473
|
json(res, 400, { error: "renders must be a project directory, not a symbolic link" });
|
|
1335
1474
|
return true;
|
|
1336
1475
|
}
|
|
1337
|
-
const stage = mkdtempSync(
|
|
1476
|
+
const stage = mkdtempSync(join6(renders, ".export-"));
|
|
1338
1477
|
const stageIdentity = lstatSync2(stage);
|
|
1339
|
-
const out =
|
|
1478
|
+
const out = join6(stage, "output.mp4");
|
|
1340
1479
|
const id2 = String(++jobSeq);
|
|
1341
1480
|
const job2 = { state: "running", done: 0, total: 0, abort: new AbortController() };
|
|
1342
1481
|
jobs.set(id2, job2);
|
|
@@ -1366,14 +1505,14 @@ async function startDev(dir, port = 0, options = {}) {
|
|
|
1366
1505
|
const candidate = n === 1 ? name : `${name.slice(0, -4)}-${n}.mp4`;
|
|
1367
1506
|
if (occupied.has(candidate)) continue;
|
|
1368
1507
|
try {
|
|
1369
|
-
linkSync(out,
|
|
1508
|
+
linkSync(out, join6(renders, candidate));
|
|
1370
1509
|
} catch (error) {
|
|
1371
1510
|
const code = error.code;
|
|
1372
1511
|
if (code === "EEXIST") continue;
|
|
1373
1512
|
if (!["ENOTSUP", "EOPNOTSUPP", "EPERM", "EXDEV", "ENOSYS"].includes(code ?? "")) throw error;
|
|
1374
|
-
const reserved = mkdtempSync(
|
|
1513
|
+
const reserved = mkdtempSync(join6(renders, `${name.slice(0, -4)}-`));
|
|
1375
1514
|
try {
|
|
1376
|
-
renameSync4(out,
|
|
1515
|
+
renameSync4(out, join6(reserved, name));
|
|
1377
1516
|
} catch (error2) {
|
|
1378
1517
|
rmSync3(reserved, { recursive: true, force: true });
|
|
1379
1518
|
throw error2;
|
|
@@ -1426,8 +1565,8 @@ async function startDev(dir, port = 0, options = {}) {
|
|
|
1426
1565
|
requireSelectBackend();
|
|
1427
1566
|
const r = selectRequest(dir, (await readBody(req)).toString());
|
|
1428
1567
|
const taken = (path) => existsSync4(path) || [...tracks.values()].some((t) => t.out === path);
|
|
1429
|
-
let out =
|
|
1430
|
-
for (let n = 2; taken(out); n++) out =
|
|
1568
|
+
let out = join6(dir, "masks", `${r.stem}.mask`);
|
|
1569
|
+
for (let n = 2; taken(out); n++) out = join6(dir, "masks", `${r.stem}-${n}.mask`);
|
|
1431
1570
|
const id2 = String(tracks.size + 1);
|
|
1432
1571
|
const job = { out, state: { status: "running" } };
|
|
1433
1572
|
tracks.set(id2, job);
|
|
@@ -1515,7 +1654,7 @@ async function startDev(dir, port = 0, options = {}) {
|
|
|
1515
1654
|
res.end("not found");
|
|
1516
1655
|
return true;
|
|
1517
1656
|
}
|
|
1518
|
-
sendFile(req, res,
|
|
1657
|
+
sendFile(req, res, join6(presets, preset.preview));
|
|
1519
1658
|
return true;
|
|
1520
1659
|
},
|
|
1521
1660
|
async (req, res, url) => {
|
|
@@ -1547,7 +1686,7 @@ async function startDev(dir, port = 0, options = {}) {
|
|
|
1547
1686
|
},
|
|
1548
1687
|
async (req, res, url) => {
|
|
1549
1688
|
if (url.pathname !== "/api/media") return false;
|
|
1550
|
-
const media =
|
|
1689
|
+
const media = join6(dir, "media");
|
|
1551
1690
|
const files = listMediaFiles(media);
|
|
1552
1691
|
json(res, 200, { files });
|
|
1553
1692
|
return true;
|
|
@@ -1559,7 +1698,8 @@ async function startDev(dir, port = 0, options = {}) {
|
|
|
1559
1698
|
json(res, 200, analyze(dir, key, names));
|
|
1560
1699
|
void req;
|
|
1561
1700
|
return true;
|
|
1562
|
-
}
|
|
1701
|
+
},
|
|
1702
|
+
...apiRoutes(dir, apiJobs)
|
|
1563
1703
|
],
|
|
1564
1704
|
port
|
|
1565
1705
|
);
|
|
@@ -132,7 +132,7 @@ function rewriteMedia(text, path, plan) {
|
|
|
132
132
|
var PLACEHOLDER_EDGE = 640;
|
|
133
133
|
var PLACEHOLDER_SECONDS = 60;
|
|
134
134
|
async function writePlaceholders(files, wanted) {
|
|
135
|
-
const { frame } = await import("./chunk-
|
|
135
|
+
const { frame } = await import("./chunk-4VE54E4U.mjs");
|
|
136
136
|
const ffmpeg = requireTool("ffmpeg");
|
|
137
137
|
const work = mkdtempSync(join(tmpdir(), "higgsedit-placeholder-"));
|
|
138
138
|
try {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire as __higgseditRequire } from "node:module";
|
|
3
|
+
import { fileURLToPath as __higgseditPath } from "node:url";
|
|
4
|
+
const require = __higgseditRequire(import.meta.url);
|
|
5
|
+
const __filename = __higgseditPath(import.meta.url);
|
|
6
|
+
const __dirname = __filename.slice(0, __filename.lastIndexOf("/"));
|
|
7
|
+
import {
|
|
8
|
+
ORT_FILES,
|
|
9
|
+
ensureModel,
|
|
10
|
+
mediaSeconds,
|
|
11
|
+
modelCached,
|
|
12
|
+
modelDir,
|
|
13
|
+
ortDir,
|
|
14
|
+
point,
|
|
15
|
+
prompts,
|
|
16
|
+
runSegment,
|
|
17
|
+
segment,
|
|
18
|
+
segmentBundle
|
|
19
|
+
} from "./chunk-Y7SAX2P4.mjs";
|
|
20
|
+
import "./chunk-7PYXSJS7.mjs";
|
|
21
|
+
import "./chunk-UDL5AHKO.mjs";
|
|
22
|
+
import "./chunk-LTKMGZ6T.mjs";
|
|
23
|
+
import "./chunk-P2BGHTRR.mjs";
|
|
24
|
+
import "./chunk-PAHMD6SU.mjs";
|
|
25
|
+
import "./chunk-NUXTKQNM.mjs";
|
|
26
|
+
import "./chunk-2VKQHJMM.mjs";
|
|
27
|
+
import "./chunk-BV5COOQZ.mjs";
|
|
28
|
+
import "./chunk-F7YUKEI5.mjs";
|
|
29
|
+
import "./chunk-MCVD2ZQV.mjs";
|
|
30
|
+
import "./chunk-75WO2NOF.mjs";
|
|
31
|
+
export {
|
|
32
|
+
ORT_FILES,
|
|
33
|
+
ensureModel,
|
|
34
|
+
mediaSeconds,
|
|
35
|
+
modelCached,
|
|
36
|
+
modelDir,
|
|
37
|
+
ortDir,
|
|
38
|
+
point,
|
|
39
|
+
prompts,
|
|
40
|
+
runSegment,
|
|
41
|
+
segment,
|
|
42
|
+
segmentBundle
|
|
43
|
+
};
|