@higgsfield/video-editor-mcp 1.1.1-dev.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.
Files changed (59) hide show
  1. package/README.md +63 -26
  2. package/dist/cli.mjs +284 -112
  3. package/dist/guide.md +2 -3
  4. package/dist/index.html +1 -1
  5. package/dist/launcher.mjs +34 -14
  6. package/dist/server.mjs +209 -265
  7. package/node_modules/@higgsfield/higgsedit/dist/api.d.ts +481 -0
  8. package/node_modules/@higgsfield/higgsedit/dist/{chunk-PFQYUJQL.mjs → chunk-3XVMJSV2.mjs} +1 -1
  9. package/node_modules/@higgsfield/higgsedit/dist/{chunk-F4CMIG5G.mjs → chunk-4VE54E4U.mjs} +3 -1
  10. package/node_modules/@higgsfield/higgsedit/dist/{chunk-2JYLVSVV.mjs → chunk-55JARHLA.mjs} +205 -65
  11. package/node_modules/@higgsfield/higgsedit/dist/{chunk-YLVXQIPX.mjs → chunk-E4N76T2R.mjs} +1 -1
  12. package/node_modules/@higgsfield/higgsedit/dist/{chunk-OCIZ36XD.mjs → chunk-ECRSOIKI.mjs} +1 -1
  13. package/node_modules/@higgsfield/higgsedit/dist/{chunk-IEPPEDEU.mjs → chunk-ENALCTAD.mjs} +1 -1
  14. package/node_modules/@higgsfield/higgsedit/dist/chunk-HY2E7IWX.mjs +43 -0
  15. package/node_modules/@higgsfield/higgsedit/dist/{chunk-NARTAWRN.mjs → chunk-HYCUOCUQ.mjs} +9 -4
  16. package/node_modules/@higgsfield/higgsedit/dist/{chunk-C3GSB3XZ.mjs → chunk-NLEIBCAK.mjs} +1 -7
  17. package/node_modules/@higgsfield/higgsedit/dist/{chunk-2WKNH7OL.mjs → chunk-O3AVYZ6U.mjs} +1 -1
  18. package/node_modules/@higgsfield/higgsedit/dist/chunk-UXO22E3P.mjs +22 -0
  19. package/node_modules/@higgsfield/higgsedit/dist/{chunk-BH2QF635.mjs → chunk-V5XTCFYK.mjs} +4 -100
  20. package/node_modules/@higgsfield/higgsedit/dist/chunk-W2TANYEH.mjs +118 -0
  21. package/node_modules/@higgsfield/higgsedit/dist/{chunk-DVN4LUXV.mjs → chunk-XY5CCUOZ.mjs} +2 -6
  22. package/node_modules/@higgsfield/higgsedit/dist/{chunk-KIWGVAPJ.mjs → chunk-Y7SAX2P4.mjs} +8 -12
  23. package/node_modules/@higgsfield/higgsedit/dist/chunk-YNAIRXOP.mjs +22 -0
  24. package/node_modules/@higgsfield/higgsedit/dist/{chunk-WENFTPXQ.mjs → chunk-ZO3NDQD3.mjs} +1 -1
  25. package/node_modules/@higgsfield/higgsedit/dist/higgsedit.mjs +13 -13
  26. package/node_modules/@higgsfield/higgsedit/package.json +4 -1
  27. package/package.json +4 -4
  28. package/plugin/.codex-plugin/plugin.json +1 -1
  29. package/plugin/skills/video-editing/SKILL.md +109 -95
  30. package/plugin/mcp.json +0 -10
  31. package/plugin/plugin.json +0 -41
  32. package/plugin/skills/motion-craft/SKILL.md +0 -87
  33. package/plugin/skills/motion-craft/agents/openai.yaml +0 -6
  34. package/plugin/skills/motion-craft/references/causality.md +0 -25
  35. package/plugin/skills/motion-craft/references/composition.md +0 -20
  36. package/plugin/skills/motion-craft/references/easing.md +0 -19
  37. package/plugin/skills/motion-craft/references/qc.md +0 -33
  38. package/plugin/skills/motion-craft/references/recipes.md +0 -31
  39. package/plugin/skills/motion-craft/references/stepped.md +0 -43
  40. package/plugin/skills/motion-craft/references/timing.md +0 -60
  41. package/plugin/skills/motion-craft/references/tsx.md +0 -60
  42. package/plugin/skills/motion-craft/references/typography.md +0 -22
  43. package/plugin/skills/video-editing/agents/openai.yaml +0 -6
  44. package/plugin/skills/video-editing/references/animation-contract.md +0 -45
  45. package/plugin/skills/video-editing/references/assembly.md +0 -58
  46. package/plugin/skills/video-editing/references/caption-systems.md +0 -29
  47. package/plugin/skills/video-editing/references/caption-titling.md +0 -41
  48. package/plugin/skills/video-editing/references/clip-geometry.md +0 -37
  49. package/plugin/skills/video-editing/references/compose.md +0 -283
  50. package/plugin/skills/video-editing/references/editor-measured.md +0 -37
  51. package/plugin/skills/video-editing/references/failure-modes.md +0 -37
  52. package/plugin/skills/video-editing/references/local-setup.md +0 -25
  53. package/plugin/skills/video-editing/references/motion-language.md +0 -27
  54. package/plugin/skills/video-editing/references/provenance.md +0 -7
  55. package/plugin/skills/video-editing/references/sandbox.md +0 -31
  56. package/plugin/skills/video-editing/references/shot-blueprints.md +0 -24
  57. package/plugin/skills/video-editing/references/title-animation.md +0 -29
  58. package/plugin/skills/video-editing/references/tsx-projects.md +0 -187
  59. 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
- } from "./chunk-NARTAWRN.mjs";
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-YLVXQIPX.mjs";
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-WENFTPXQ.mjs";
52
- import "./chunk-QJTILFBS.mjs";
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 "./chunk-NUXTKQNM.mjs";
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 join5 } from "node:path";
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 = join(import.meta.dirname, "../../editor");
74
- var EDITOR_DIST = PACKAGED ? join(import.meta.dirname, "editor") : join(EDITOR, "dist");
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 = join(EDITOR_DIST, ".vite/manifest.json");
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 = join(dir, e.name);
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 = () => [join(EDITOR, "src"), ...PACKAGES.map((p) => join(EDITOR, `../../packages/${p}/src`))].filter((dir) => existsSync(dir));
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 the plugin build, or run `higgsedit dev` from a checkout");
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: join(EDITOR, "vite.config.ts"), logLevel: "error", mode: "production" });
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}`) : join(EDITOR_DIST, 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 join2 } from "node:path";
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 = join2(target, part);
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 = join2(this.dir, path);
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 = join2(this.dir, ".higgsedit-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 = join2(this.cacheDir(), "history.json");
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(join2(this.cacheDir(), "history.json"), { bigint: true });
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 = join2(cache, "history.json");
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 join3, posix } from "node:path";
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 = join3(dir, PRERENDER_DIR);
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(join3(this.dir, f));
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(join3(this.dir, f));
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(join3(this.dir, dir)).filter((n) => re.test(n)).map((n) => posix.join(dir, n)).toSorted();
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(join3(this.root, `${id}.json`), "utf8"));
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 = join3(this.root, name);
776
+ const file = join4(this.root, name);
639
777
  if (!existsSync2(file)) return void 0;
640
- const meta = join3(this.root, `${name.slice(0, 32)}.json`);
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 = join3(this.root, "..", ".gitignore");
898
+ const ignore = join4(this.root, "..", ".gitignore");
761
899
  if (!existsSync2(ignore)) writeFileSync2(ignore, "*\n");
762
- const colour = join3(this.root, `${id}.part.mp4`);
763
- const matte = join3(this.root, `${id}.matte.part.mp4`);
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, join3(this.root, `${id}.mp4`));
1006
+ renameSync2(colour, join4(this.root, `${id}.mp4`));
869
1007
  if (opaque) rmSync(matte, { force: true });
870
- else renameSync2(matte, join3(this.root, `${id}.matte.mp4`));
871
- entry.bytes = statSync2(join3(this.root, `${id}.mp4`)).size + (opaque ? 0 : statSync2(join3(this.root, `${id}.matte.mp4`)).size);
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(join3(this.root, `${id}.json.part`), JSON.stringify(entry));
874
- renameSync2(join3(this.root, `${id}.json.part`), join3(this.root, `${id}.json`));
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(join3(this.root, n));
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(join3(this.root, `${id}${ext}`), { force: true });
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 join4 } from "node:path";
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 = join4(dir, ".higgsedit-cache", "proxies");
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(join4(this.dir, rel2));
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 && join4(this.root, `${key}.mp4`);
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(join4(this.root, `${version.key}.mp4`)) };
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 = join4(this.dir, rel2);
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 = join4(this.root, `${key}.mp4`);
1050
- const part = join4(this.root, `${key}.${randomUUID2()}.part.mp4`);
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: basename(file, extname(file)),
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(join5(dir, f), "utf8"));
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(join5(dir, f), "utf8");
1167
- mtime = Math.max(mtime, statSync4(join5(dir, f)).mtimeMs);
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 = join5(dir, "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(join5(renders, ".export-"));
1476
+ const stage = mkdtempSync(join6(renders, ".export-"));
1338
1477
  const stageIdentity = lstatSync2(stage);
1339
- const out = join5(stage, "output.mp4");
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, join5(renders, candidate));
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(join5(renders, `${name.slice(0, -4)}-`));
1513
+ const reserved = mkdtempSync(join6(renders, `${name.slice(0, -4)}-`));
1375
1514
  try {
1376
- renameSync4(out, join5(reserved, name));
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 = join5(dir, "masks", `${r.stem}.mask`);
1430
- for (let n = 2; taken(out); n++) out = join5(dir, "masks", `${r.stem}-${n}.mask`);
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, join5(presets, preset.preview));
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 = join5(dir, "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-F4CMIG5G.mjs");
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 {
@@ -17,7 +17,7 @@ import {
17
17
  selectFrame,
18
18
  selectStatus,
19
19
  track
20
- } from "./chunk-WENFTPXQ.mjs";
20
+ } from "./chunk-ZO3NDQD3.mjs";
21
21
  import "./chunk-QJTILFBS.mjs";
22
22
  import "./chunk-PAHMD6SU.mjs";
23
23
  import "./chunk-BV5COOQZ.mjs";
@@ -14,7 +14,7 @@ import {
14
14
  presetsDir,
15
15
  readManifest,
16
16
  runPresets
17
- } from "./chunk-YLVXQIPX.mjs";
17
+ } from "./chunk-E4N76T2R.mjs";
18
18
  import "./chunk-MV2JTTBQ.mjs";
19
19
  import "./chunk-PAHMD6SU.mjs";
20
20
  import "./chunk-F7YUKEI5.mjs";
@@ -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
+ };