@higgsfield/video-editor-mcp 1.0.7 → 1.1.1-dev.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/THIRD_PARTY_NOTICES.md +24 -24
- package/dist/cli.mjs +134 -125
- package/dist/index.html +136 -133
- package/dist/launcher.mjs +90 -90
- package/dist/server.mjs +1553 -1389
- package/node_modules/@higgsfield/higgsedit/README.md +181 -1
- package/node_modules/@higgsfield/higgsedit/dist/capture-page.js +902 -314
- package/node_modules/@higgsfield/higgsedit/dist/chunk-2JYLVSVV.mjs +1594 -0
- package/node_modules/@higgsfield/higgsedit/dist/chunk-2WKNH7OL.mjs +16 -0
- package/node_modules/@higgsfield/higgsedit/dist/{chunk-OMKZ5BXM.mjs → chunk-5LTTL3Q3.mjs} +1 -1
- package/node_modules/@higgsfield/higgsedit/dist/chunk-7PYXSJS7.mjs +717 -0
- package/node_modules/@higgsfield/higgsedit/dist/{chunk-MLJAIZGP.mjs → chunk-BH2QF635.mjs} +33 -326
- package/node_modules/@higgsfield/higgsedit/dist/{chunk-WZR5PZM7.mjs → chunk-BV5COOQZ.mjs} +250 -161
- package/node_modules/@higgsfield/higgsedit/dist/{chunk-HTCYVKFX.mjs → chunk-C3GSB3XZ.mjs} +8 -7
- package/node_modules/@higgsfield/higgsedit/dist/{chunk-CYH2EUQV.mjs → chunk-DVN4LUXV.mjs} +13 -4
- package/node_modules/@higgsfield/higgsedit/dist/chunk-F4CMIG5G.mjs +36 -0
- package/node_modules/@higgsfield/higgsedit/dist/chunk-IEPPEDEU.mjs +33 -0
- package/node_modules/@higgsfield/higgsedit/dist/{chunk-UVAZ6I6U.mjs → chunk-KIWGVAPJ.mjs} +16 -12
- package/node_modules/@higgsfield/higgsedit/dist/{chunk-L6QDNSL3.mjs → chunk-L5LWBA6A.mjs} +5 -5
- package/node_modules/@higgsfield/higgsedit/dist/chunk-LMQR3DHT.mjs +37 -0
- package/node_modules/@higgsfield/higgsedit/dist/chunk-LTKMGZ6T.mjs +58 -0
- package/node_modules/@higgsfield/higgsedit/dist/{chunk-VI6IV6JP.mjs → chunk-MV2JTTBQ.mjs} +65 -47
- package/node_modules/@higgsfield/higgsedit/dist/{chunk-4D6AXU4B.mjs → chunk-NARTAWRN.mjs} +50 -30
- package/node_modules/@higgsfield/higgsedit/dist/{chunk-27NBQ5FA.mjs → chunk-NUXTKQNM.mjs} +33 -21
- package/node_modules/@higgsfield/higgsedit/dist/chunk-OCIZ36XD.mjs +40 -0
- package/node_modules/@higgsfield/higgsedit/dist/{chunk-MK6PL7N4.mjs → chunk-OF6RMSM2.mjs} +19 -17
- package/node_modules/@higgsfield/higgsedit/dist/{chunk-6BETMESC.mjs → chunk-P2BGHTRR.mjs} +2 -2
- package/node_modules/@higgsfield/higgsedit/dist/{chunk-EKFC4S4M.mjs → chunk-PAHMD6SU.mjs} +5 -0
- package/node_modules/@higgsfield/higgsedit/dist/{chunk-QVLAEV3U.mjs → chunk-PFQYUJQL.mjs} +3 -4
- package/node_modules/@higgsfield/higgsedit/dist/chunk-QJTILFBS.mjs +335 -0
- package/node_modules/@higgsfield/higgsedit/dist/chunk-QQ3GGKDN.mjs +21 -0
- package/node_modules/@higgsfield/higgsedit/dist/chunk-TH7TK67K.mjs +23 -0
- package/node_modules/@higgsfield/higgsedit/dist/{chunk-SIMO6I3D.mjs → chunk-UDL5AHKO.mjs} +163 -158
- package/node_modules/@higgsfield/higgsedit/dist/chunk-UMWOUQN3.mjs +60 -0
- package/node_modules/@higgsfield/higgsedit/dist/{chunk-3AIM3THH.mjs → chunk-VLM5EOWJ.mjs} +4 -4
- package/node_modules/@higgsfield/higgsedit/dist/chunk-WENFTPXQ.mjs +403 -0
- package/node_modules/@higgsfield/higgsedit/dist/chunk-YLVXQIPX.mjs +464 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/.vite/manifest.json +559 -506
- package/node_modules/@higgsfield/higgsedit/dist/editor/assets/agent-corner-bottom-q4hZltI-.svg +431 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/assets/agent-corner-top-BJYI-I24.svg +431 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/assets/confirmation-BTnwl3LS.css +1 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/assets/drag-Bt_t4HZO.css +1 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/assets/local-B3OTAV_A.css +1 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/assets/menubar-2Y1Y2XJt.css +1 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/assets/model-DdE4hCgf.css +1 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/assets/{IconText1-CzbCrP7v.css → notifications-B08L1jSe.css} +1 -1
- package/node_modules/@higgsfield/higgsedit/dist/editor/assets/{panel-g852pZf_.css → panel-BRN_AbBM.css} +1 -1
- package/node_modules/@higgsfield/higgsedit/dist/editor/assets/panel-CYELNqrv.css +1 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/assets/panel-DYLpT6oA.css +1 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/assets/panel-v84HaGTZ.css +1 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/IconChainLink1-Bxb1Or0F.js +1 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/{IconCheckmark2Medium-daaES9uF.js → IconCheckmark2Medium-hegaz9mQ.js} +1 -1
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/{IconChevronDownSmall-BhTBJk26.js → IconChevronDownSmall-DaruzJwU.js} +1 -1
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/IconClosedCaptioning-BPqcqKsD.js +1 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/{IconCrossMedium-acfnCXOM.js → IconCrossMedium-BsPFJzc6.js} +1 -1
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/IconOpenaiFilled-D1fzthmi.js +1 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/{IconPause-pTeVSvUe.js → IconPause-ClNg8pao.js} +1 -1
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/{IconPlayFilled-CwdgIuDG.js → IconPlayFilled-CHb-7GSx.js} +1 -1
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/IconSparklesSoftFilled-o7o33KRn.js +1 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/ai-composer-B9h-eaki.js +567 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/auto-layout-BO20Tg4B.js +1 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/{button-CjG6GcgF.js → button-Cg0U0zpV.js} +1 -1
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/canvas-Bw4CVzfw.js +1 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/capabilities-BCd4-zPF.js +1 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/commands-QeSqAbCt.js +1 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/components-tXMtIZNL.js +51 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/confirmation-CO2PHR_z.js +1 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/dom-6aoQn29n.js +1 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/drag-Dw8b_vO2.js +1 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/{editing-CjWbiCxv.js → editing-Cy-SaOyB.js} +1 -1
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/edits-CqLBqir0.js +1 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/export-dialog-BjBskOe3.js +2 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/factories-Bw_iQrUL.js +1 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/id3-CSR0NCRI.js +2 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/import-DP49i4ke.js +1 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/import-error-CMz9sLPe.js +1 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/ingest-DsmIc415.js +1 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/jsx-runtime-B5yqYJvp.js +1 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/{media-Bwjxu-0U.js → media-DQw6vU_T.js} +1 -1
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/menubar-D31srALX.js +1 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/meters-D0nZVEJs.js +1 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/mode-DEG6doKF.js +1 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/model-Duf9fszJ.js +2 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/model-DyAkLA69.js +1 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/notifications-DqWZNIrK.js +9 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/palette-HT83gWgL.js +1 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/panel-B5Mvm6dg.js +7 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/panel-BC90wyvd.js +1 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/panel-Bw_ZJ2JI.js +3 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/panel-DNS060Nt.js +3 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/panel-DSgxIrEq.js +2 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/panel-DgBttE5O.js +1 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/panel-assets-_T2bHsHW.js +771 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/panel-inspector-DfHvAt5X.js +2 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/panel-nBZvHlkA.js +1 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/panel-timeline-DsCpJVSt.js +327 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/panel-viewport-CEhZwZ67.js +280 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/projects-2_tW9Do7.js +1 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/react-dom-1uB7Vbbb.js +1 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/schemas-CddYAOcj.js +64 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/session-BpFeKAI2.js +1 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/src-BLG3tYBa.js +2 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/styles-e2n2A_JN.js +284 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/supercomputer-CuV0zYKd.js +24 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/tabs-XeAgs9cs.js +1 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/toggle-DbNEaSIl.js +1 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/transport-C8Ll3qFl.js +69 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/transport-bar-Dv1LPFbu.js +1 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/useOpenInteractionType-HWSfSsp2.js +1 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/useRenderElement-D4SOg3N_.js +1 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/value-EB7YmAdS.js +1 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/writeback-support-fj5rA9k3.js +1 -0
- package/node_modules/@higgsfield/higgsedit/dist/editor/editor-B-WAkLt7.js +372 -0
- package/node_modules/@higgsfield/higgsedit/dist/higgsedit.mjs +39 -24
- package/node_modules/@higgsfield/higgsedit/dist/jsx/hooks.d.ts +26 -0
- package/node_modules/@higgsfield/higgsedit/dist/jsx/index.d.ts +17 -0
- package/node_modules/@higgsfield/higgsedit/dist/jsx/inspect.d.ts +42 -0
- package/node_modules/@higgsfield/higgsedit/dist/jsx/jsx-runtime.d.ts +28 -0
- package/node_modules/@higgsfield/higgsedit/dist/jsx/svg.d.ts +9 -0
- package/node_modules/@higgsfield/higgsedit/dist/jsx/time.d.ts +6 -0
- package/node_modules/@higgsfield/higgsedit/dist/jsx/types.d.ts +420 -0
- package/node_modules/@higgsfield/higgsedit/package.json +28 -20
- package/package.json +4 -3
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/plugin.json +1 -1
- package/plugin/skills/video-editing/references/caption-titling.md +1 -1
- package/plugin/skills/video-editing/references/local-setup.md +3 -3
- package/node_modules/@higgsfield/higgsedit/dist/chunk-C6SASQHE.mjs +0 -424
- package/node_modules/@higgsfield/higgsedit/dist/chunk-FZ5U36SQ.mjs +0 -83
- package/node_modules/@higgsfield/higgsedit/dist/chunk-QLPCYN6K.mjs +0 -292
- package/node_modules/@higgsfield/higgsedit/dist/editor/assets/edit-drag-D2EV5ddd.css +0 -1
- package/node_modules/@higgsfield/higgsedit/dist/editor/assets/editor-CuRHT75g.css +0 -1
- package/node_modules/@higgsfield/higgsedit/dist/editor/assets/panel-DnuQD7ja.css +0 -1
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/FieldControl-B0fenhBJ.js +0 -2
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/IconChainLink1-Cwv1X7hi.js +0 -1
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/IconEyeOpen-BqTT9_VV.js +0 -1
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/IconEyeSlash-Whv58c1m.js +0 -1
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/IconPencil-Dr17k_7e.js +0 -1
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/IconPlay-BroH3ziH.js +0 -1
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/IconPlusSmall-43T9aE3d.js +0 -1
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/IconText1-BaP_p9DW.js +0 -1
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/IconTrashCan-DX2w2AIC.js +0 -1
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/ai-composer-C-l0eIdP.js +0 -560
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/captions-Cd-bDG3G.js +0 -1
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/clip-highlight-DpAvcA4x.js +0 -1
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/components-e3ZbUVns.js +0 -1
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/disposables-CwUPjcuA.js +0 -1
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/drag-HLBkto5o.js +0 -1
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/dropdown-BJ04G_X3.js +0 -1
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/edit-drag-Bg6LggKC.js +0 -1
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/edits-a2IixhrN.js +0 -1
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/export-dialog-4I4gqI5V.js +0 -2
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/face-tracker-DzVfjVOO.js +0 -1
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/fit-imported-media-CaCB0x0b.js +0 -1
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/generation-queue-DuL2F4-_.js +0 -1
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/import-error-CMARA0Hy.js +0 -1
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/jsx-runtime-B-hcVAMW.js +0 -1
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/menubar-Bs26OdwP.js +0 -1
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/meters-YEr9OQjq.js +0 -1
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/paints-DS2Msf91.js +0 -67
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/palette-CcS9IcAC.js +0 -1
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/panel-7M3uQokX.js +0 -1
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/panel-BBK1NyG-.js +0 -1
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/panel-CHZTs7By.js +0 -4
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/panel-DaSOa3MP.js +0 -2
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/panel-DlK3RYVo.js +0 -1
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/panel-Dst5OKWu.js +0 -2
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/panel-H_ZoDiKK.js +0 -2
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/panel-Rn9Z7Eek.js +0 -3
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/panel-assets-FAghGFG3.js +0 -678
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/panel-inspector-DEqULBNF.js +0 -260
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/panel-timeline-0GYNz1uc.js +0 -333
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/panel-viewport-DOWjJi5-.js +0 -317
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/projects-BBJc7h5_.js +0 -2
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/session-B9sZaFKC.js +0 -1
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/supercomputer-CX-LCL6Y.js +0 -24
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/tabs-CvKQ-eas.js +0 -1
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/toggle-CrzrwiDZ.js +0 -1
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/transport-BgRfk-Qq.js +0 -69
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/transport-bar-C9nqAfE2.js +0 -1
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/typography-C5-w9LOu-kD3uLKkf.js +0 -1
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/useOpenInteractionType-DbrQ6Ehu.js +0 -1
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/useRenderElement-bbxXdYq0.js +0 -1
- package/node_modules/@higgsfield/higgsedit/dist/editor/chunks/writeback-support-BZfNpkmB.js +0 -1
- package/node_modules/@higgsfield/higgsedit/dist/editor/editor-Csn2_T_e.js +0 -293
- package/node_modules/@higgsfield/higgsedit/dist/export-settings.mjs +0 -75
- package/node_modules/@higgsfield/higgsedit/dist/history.mjs +0 -295
- package/node_modules/@higgsfield/higgsedit/dist/media-upload.mjs +0 -381
- package/node_modules/@higgsfield/higgsedit/dist/proxy.mjs +0 -170
|
@@ -0,0 +1,1594 @@
|
|
|
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
|
+
api,
|
|
9
|
+
openCapture,
|
|
10
|
+
render
|
|
11
|
+
} from "./chunk-NARTAWRN.mjs";
|
|
12
|
+
import {
|
|
13
|
+
addPreset,
|
|
14
|
+
presetCatalogue,
|
|
15
|
+
presetsDir,
|
|
16
|
+
readManifest
|
|
17
|
+
} from "./chunk-YLVXQIPX.mjs";
|
|
18
|
+
import {
|
|
19
|
+
analyze,
|
|
20
|
+
applyEdits
|
|
21
|
+
} from "./chunk-MV2JTTBQ.mjs";
|
|
22
|
+
import {
|
|
23
|
+
MEDIA_UPLOAD_TEMP_PREFIX,
|
|
24
|
+
MediaUploads,
|
|
25
|
+
PACKAGED,
|
|
26
|
+
json,
|
|
27
|
+
listMediaFiles,
|
|
28
|
+
packagedAsset,
|
|
29
|
+
pageBundle,
|
|
30
|
+
readBody,
|
|
31
|
+
sendFile,
|
|
32
|
+
startServer
|
|
33
|
+
} from "./chunk-7PYXSJS7.mjs";
|
|
34
|
+
import "./chunk-UDL5AHKO.mjs";
|
|
35
|
+
import {
|
|
36
|
+
fontFiles,
|
|
37
|
+
inside,
|
|
38
|
+
readSources,
|
|
39
|
+
rel,
|
|
40
|
+
sourceFiles
|
|
41
|
+
} from "./chunk-LTKMGZ6T.mjs";
|
|
42
|
+
import {
|
|
43
|
+
CHROME_VERSION
|
|
44
|
+
} from "./chunk-P2BGHTRR.mjs";
|
|
45
|
+
import {
|
|
46
|
+
SelectError,
|
|
47
|
+
parsePrompt,
|
|
48
|
+
requireSelectBackend,
|
|
49
|
+
select,
|
|
50
|
+
track
|
|
51
|
+
} from "./chunk-WENFTPXQ.mjs";
|
|
52
|
+
import "./chunk-QJTILFBS.mjs";
|
|
53
|
+
import {
|
|
54
|
+
ffmpeg,
|
|
55
|
+
requireTool
|
|
56
|
+
} from "./chunk-PAHMD6SU.mjs";
|
|
57
|
+
import "./chunk-NUXTKQNM.mjs";
|
|
58
|
+
import "./chunk-2VKQHJMM.mjs";
|
|
59
|
+
import "./chunk-BV5COOQZ.mjs";
|
|
60
|
+
import {
|
|
61
|
+
parseTime
|
|
62
|
+
} from "./chunk-F7YUKEI5.mjs";
|
|
63
|
+
import "./chunk-MCVD2ZQV.mjs";
|
|
64
|
+
import "./chunk-75WO2NOF.mjs";
|
|
65
|
+
|
|
66
|
+
// src/dev.ts
|
|
67
|
+
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";
|
|
69
|
+
|
|
70
|
+
// src/editor-build.ts
|
|
71
|
+
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");
|
|
75
|
+
var EDITOR_MANIFEST = "editor/.vite/manifest.json";
|
|
76
|
+
var MANIFEST = join(EDITOR_DIST, ".vite/manifest.json");
|
|
77
|
+
function newest(dir) {
|
|
78
|
+
let t = 0;
|
|
79
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
80
|
+
const p = join(dir, e.name);
|
|
81
|
+
t = Math.max(t, e.isDirectory() ? newest(p) : statSync(p).mtimeMs);
|
|
82
|
+
}
|
|
83
|
+
return t;
|
|
84
|
+
}
|
|
85
|
+
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));
|
|
87
|
+
var MissingEditorError = class extends Error {
|
|
88
|
+
constructor() {
|
|
89
|
+
super("this higgsedit build has no editor page: install the plugin build, or run `higgsedit dev` from a checkout");
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
var building;
|
|
93
|
+
async function editorEntry(log = () => {
|
|
94
|
+
}) {
|
|
95
|
+
if (PACKAGED) {
|
|
96
|
+
const manifest = packagedAsset(EDITOR_MANIFEST);
|
|
97
|
+
if (!existsSync(manifest)) throw new MissingEditorError();
|
|
98
|
+
return manifestEntry(readFileSync(manifest, "utf8"));
|
|
99
|
+
}
|
|
100
|
+
const stale = !existsSync(MANIFEST) || inputs().some((dir) => newest(dir) > statSync(MANIFEST).mtimeMs);
|
|
101
|
+
if (stale) {
|
|
102
|
+
building ??= (async () => {
|
|
103
|
+
log("building the editor page\u2026");
|
|
104
|
+
const t0 = performance.now();
|
|
105
|
+
const { build } = await import("vite");
|
|
106
|
+
const env = process.env.NODE_ENV;
|
|
107
|
+
process.env.NODE_ENV = "production";
|
|
108
|
+
try {
|
|
109
|
+
await build({ configFile: join(EDITOR, "vite.config.ts"), logLevel: "error", mode: "production" });
|
|
110
|
+
} finally {
|
|
111
|
+
process.env.NODE_ENV = env;
|
|
112
|
+
}
|
|
113
|
+
log(`built the editor page in ${Math.round(performance.now() - t0)} ms`);
|
|
114
|
+
})().finally(() => {
|
|
115
|
+
building = void 0;
|
|
116
|
+
});
|
|
117
|
+
await building;
|
|
118
|
+
}
|
|
119
|
+
return manifestEntry(readFileSync(MANIFEST, "utf8"));
|
|
120
|
+
}
|
|
121
|
+
var editorFile = (rel2) => PACKAGED ? packagedAsset(`editor/${rel2}`) : join(EDITOR_DIST, rel2);
|
|
122
|
+
function manifestEntry(text) {
|
|
123
|
+
const manifest = JSON.parse(text);
|
|
124
|
+
const entry = Object.values(manifest).find((m) => m.isEntry);
|
|
125
|
+
if (!entry) throw new Error("the editor manifest has no entry");
|
|
126
|
+
return { script: entry.file, styles: entry.css ?? [] };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// src/export-settings.ts
|
|
130
|
+
var capabilities;
|
|
131
|
+
var hasEncoder = (output, name) => new RegExp(`^\\s*[A-Z.]{6}\\s+${name}\\s`, "m").test(output);
|
|
132
|
+
function nativeExportCapabilities() {
|
|
133
|
+
capabilities ??= new Promise((resolve, reject) => {
|
|
134
|
+
const child = ffmpeg(["-encoders"]);
|
|
135
|
+
let output = "";
|
|
136
|
+
const timeout = setTimeout(() => {
|
|
137
|
+
child.kill("SIGKILL");
|
|
138
|
+
reject(new Error("native encoder inspection timed out"));
|
|
139
|
+
}, 15e3);
|
|
140
|
+
child.stdout.on("data", (chunk) => {
|
|
141
|
+
output += chunk.toString();
|
|
142
|
+
});
|
|
143
|
+
child.once("error", (error) => {
|
|
144
|
+
clearTimeout(timeout);
|
|
145
|
+
reject(error);
|
|
146
|
+
});
|
|
147
|
+
child.once("close", (code) => {
|
|
148
|
+
clearTimeout(timeout);
|
|
149
|
+
if (code !== 0) return reject(new Error("could not inspect native video encoders"));
|
|
150
|
+
const videoCodecs = [];
|
|
151
|
+
if (hasEncoder(output, "libx264")) videoCodecs.push("avc");
|
|
152
|
+
if (hasEncoder(output, "libx265")) videoCodecs.push("hevc");
|
|
153
|
+
resolve({
|
|
154
|
+
containers: videoCodecs.length ? ["mp4"] : [],
|
|
155
|
+
videoCodecs,
|
|
156
|
+
audioCodecs: hasEncoder(output, "aac") ? ["aac"] : [],
|
|
157
|
+
sampleRates: [44100, 48e3, 96e3],
|
|
158
|
+
audioOnly: false
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
}).catch((error) => {
|
|
162
|
+
capabilities = void 0;
|
|
163
|
+
throw error;
|
|
164
|
+
});
|
|
165
|
+
return capabilities;
|
|
166
|
+
}
|
|
167
|
+
function object(value, label) {
|
|
168
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
169
|
+
throw new Error(`${label} must be an object`);
|
|
170
|
+
return value;
|
|
171
|
+
}
|
|
172
|
+
function number(value, label, min, max, integer = false) {
|
|
173
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < min || value > max || integer && !Number.isInteger(value))
|
|
174
|
+
throw new Error(
|
|
175
|
+
`${label} must be ${integer ? "an integer" : "a number"} between ${min} and ${max}`
|
|
176
|
+
);
|
|
177
|
+
return value;
|
|
178
|
+
}
|
|
179
|
+
function member(value, allowed, label) {
|
|
180
|
+
const match = allowed.find((item) => item === value);
|
|
181
|
+
if (match === void 0) throw new Error(`unsupported ${label}: ${String(value)}`);
|
|
182
|
+
return match;
|
|
183
|
+
}
|
|
184
|
+
function validateRenderRequest(input, supported) {
|
|
185
|
+
const body = object(input, "render request");
|
|
186
|
+
const out = {
|
|
187
|
+
depth: body.depth === void 0 ? 8 : member(body.depth, [8, 10], "bit depth"),
|
|
188
|
+
allowFontFallback: body.allowFontFallback === true
|
|
189
|
+
};
|
|
190
|
+
if (body.fileName !== void 0) {
|
|
191
|
+
if (typeof body.fileName !== "string" || !body.fileName.trim() || body.fileName.length > 180 || Array.from(body.fileName).some((char) => char.charCodeAt(0) < 32))
|
|
192
|
+
throw new Error("fileName must be a nonempty filename of at most 180 characters");
|
|
193
|
+
out.fileName = body.fileName;
|
|
194
|
+
}
|
|
195
|
+
if (body.sceneId !== void 0) {
|
|
196
|
+
if (typeof body.sceneId !== "string" || !body.sceneId || body.sceneId.length > 1024)
|
|
197
|
+
throw new Error("sceneId must be a nonempty scene identifier");
|
|
198
|
+
out.sceneId = body.sceneId;
|
|
199
|
+
}
|
|
200
|
+
for (const key of ["from", "to"]) {
|
|
201
|
+
const value = body[key];
|
|
202
|
+
if (value === void 0) continue;
|
|
203
|
+
if (typeof value !== "number" && typeof value !== "string" || typeof value === "string" && value.length > 64)
|
|
204
|
+
throw new Error(`${key} must be a nonnegative time`);
|
|
205
|
+
const time = parseTime(
|
|
206
|
+
typeof value === "string" && /^\d+(\.\d+)?$/.test(value) ? Number(value) : value,
|
|
207
|
+
30
|
|
208
|
+
);
|
|
209
|
+
if (time === void 0 || !Number.isFinite(time) || time < 0)
|
|
210
|
+
throw new Error(`${key} must be a nonnegative time`);
|
|
211
|
+
out[key] = String(value);
|
|
212
|
+
}
|
|
213
|
+
if (body.spec === void 0) return out;
|
|
214
|
+
const spec = object(body.spec, "spec"), video = object(spec.video, "spec.video (audio-only export is unavailable)");
|
|
215
|
+
const width = number(video.width, "video width", 2, 8192, true), height = number(video.height, "video height", 2, 8192, true);
|
|
216
|
+
if (width % 2 || height % 2 || width * height > 35389440)
|
|
217
|
+
throw new Error("video dimensions must be even and no larger than 8K");
|
|
218
|
+
const normalized = {
|
|
219
|
+
container: member(spec.container, supported.containers, "container"),
|
|
220
|
+
video: {
|
|
221
|
+
width,
|
|
222
|
+
height,
|
|
223
|
+
fps: number(video.fps, "video fps", 1, 120),
|
|
224
|
+
codec: member(video.codec, supported.videoCodecs, "video codec")
|
|
225
|
+
},
|
|
226
|
+
audio: null
|
|
227
|
+
};
|
|
228
|
+
if (video.bitrate !== void 0)
|
|
229
|
+
normalized.video.bitrate = number(video.bitrate, "video bitrate", 1e3, 1e9, true);
|
|
230
|
+
if (spec.audio !== null) {
|
|
231
|
+
const audio = object(spec.audio, "spec.audio");
|
|
232
|
+
normalized.audio = {
|
|
233
|
+
codec: member(audio.codec, supported.audioCodecs, "audio codec"),
|
|
234
|
+
sampleRate: member(audio.sampleRate, supported.sampleRates, "audio sample rate"),
|
|
235
|
+
channels: member(audio.channels, [1, 2], "audio channels")
|
|
236
|
+
};
|
|
237
|
+
if (audio.bitrate !== void 0)
|
|
238
|
+
normalized.audio.bitrate = number(audio.bitrate, "audio bitrate", 16e3, 1024e3, true);
|
|
239
|
+
}
|
|
240
|
+
out.depth = 8;
|
|
241
|
+
out.spec = normalized;
|
|
242
|
+
return out;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// src/history.ts
|
|
246
|
+
import { randomUUID } from "node:crypto";
|
|
247
|
+
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";
|
|
249
|
+
var MAX_BYTES = 16 * 1024 * 1024;
|
|
250
|
+
var MAX_ENTRIES = 100;
|
|
251
|
+
var STALE = "edit history changed in another editor; reopen the project before editing or undoing";
|
|
252
|
+
var message = (error) => error instanceof Error ? error.message : String(error);
|
|
253
|
+
var missing = (error) => error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
254
|
+
var record = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
255
|
+
var safePath = (path) => !/[\\\0]/.test(path) && path.split("/").every((part) => part !== "" && part !== "." && part !== ".." && part !== ".higgsedit-cache") && !/^[A-Za-z]:/.test(path);
|
|
256
|
+
var identity = (stat) => `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`;
|
|
257
|
+
function encode(undo, redo, revision, text = (entry) => JSON.stringify(entryValue(entry))) {
|
|
258
|
+
return `{"version":1,"revision":${JSON.stringify(revision)},"undo":[${undo.map(text).join(",")}],"redo":[${redo.map(text).join(",")}]}`;
|
|
259
|
+
}
|
|
260
|
+
var entryValue = ({ name, files }) => ({ name, files: [...files].map(([path, texts]) => ({ path, ...texts })) });
|
|
261
|
+
function decode(text) {
|
|
262
|
+
let value;
|
|
263
|
+
try {
|
|
264
|
+
value = JSON.parse(text);
|
|
265
|
+
} catch {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
if (!record(value) || value.version !== 1 || typeof value.revision !== "string" || !Array.isArray(value.undo) || !Array.isArray(value.redo) || value.undo.length + value.redo.length > MAX_ENTRIES) return;
|
|
269
|
+
const entries = (stack) => {
|
|
270
|
+
const result = [];
|
|
271
|
+
for (const entry of stack) {
|
|
272
|
+
if (!record(entry) || typeof entry.name !== "string" || !Array.isArray(entry.files) || !entry.files.length) return;
|
|
273
|
+
const files = /* @__PURE__ */ new Map();
|
|
274
|
+
for (const file of entry.files) {
|
|
275
|
+
if (!record(file) || typeof file.path !== "string" || !safePath(file.path) || typeof file.before !== "string" || typeof file.after !== "string" || files.has(file.path)) return;
|
|
276
|
+
files.set(file.path, { before: file.before, after: file.after });
|
|
277
|
+
}
|
|
278
|
+
result.push({ name: entry.name, files });
|
|
279
|
+
}
|
|
280
|
+
return result;
|
|
281
|
+
};
|
|
282
|
+
const undo = entries(value.undo), redo = entries(value.redo);
|
|
283
|
+
return undo && redo ? { undo, redo } : void 0;
|
|
284
|
+
}
|
|
285
|
+
var EditHistory = class {
|
|
286
|
+
constructor(dir, onWrite, limits = {}) {
|
|
287
|
+
this.dir = dir;
|
|
288
|
+
this.onWrite = onWrite;
|
|
289
|
+
this.maxBytes = Math.min(MAX_BYTES, limits.maxBytes ?? MAX_BYTES);
|
|
290
|
+
this.maxEntries = Math.min(MAX_ENTRIES, limits.maxEntries ?? MAX_ENTRIES);
|
|
291
|
+
if (!Number.isSafeInteger(this.maxBytes) || this.maxBytes < 100 || !Number.isSafeInteger(this.maxEntries) || this.maxEntries < 1) throw new Error("invalid edit history limits");
|
|
292
|
+
try {
|
|
293
|
+
const saved = this.readJournal();
|
|
294
|
+
this.revision = saved.revision;
|
|
295
|
+
const parsed = saved.text === void 0 ? void 0 : decode(saved.text);
|
|
296
|
+
if (parsed) {
|
|
297
|
+
const bounded = this.bound(parsed.undo, parsed.redo);
|
|
298
|
+
this.undoStack = bounded.undo;
|
|
299
|
+
this.redoStack = bounded.redo;
|
|
300
|
+
}
|
|
301
|
+
} catch {
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
dir;
|
|
305
|
+
onWrite;
|
|
306
|
+
undoStack = [];
|
|
307
|
+
redoStack = [];
|
|
308
|
+
revision = "missing";
|
|
309
|
+
maxBytes;
|
|
310
|
+
maxEntries;
|
|
311
|
+
// Committed entries never change. Weak keys release evicted history and abandoned transactions.
|
|
312
|
+
encoded = /* @__PURE__ */ new WeakMap();
|
|
313
|
+
begin(name) {
|
|
314
|
+
if (typeof name !== "string") throw new Error("edit history name must be a string");
|
|
315
|
+
this.assertCurrent();
|
|
316
|
+
const entry = { name, files: /* @__PURE__ */ new Map() };
|
|
317
|
+
const previousWrites = /* @__PURE__ */ new Map();
|
|
318
|
+
const fileBytes = /* @__PURE__ */ new Map();
|
|
319
|
+
let entryBytes = Buffer.byteLength(encode([entry], [], randomUUID()));
|
|
320
|
+
let finished = false;
|
|
321
|
+
return {
|
|
322
|
+
onWrite: (path, text) => {
|
|
323
|
+
if (finished) throw new Error("edit history transaction already finished");
|
|
324
|
+
const prior = entry.files.get(path);
|
|
325
|
+
const current = this.readSource(path);
|
|
326
|
+
if (prior && current !== prior.after) throw new Error(`${path} changed outside the editor since ${entry.name}`);
|
|
327
|
+
const texts = { before: prior?.before ?? current, after: text };
|
|
328
|
+
const bytes = Buffer.byteLength(JSON.stringify({ path, ...texts }));
|
|
329
|
+
const nextBytes = entryBytes - (fileBytes.get(path) ?? 0) + bytes + (!prior && entry.files.size ? 1 : 0);
|
|
330
|
+
if (nextBytes > this.maxBytes) throw new Error(`edit history size limit exceeded (${this.maxBytes} bytes); split the edit or reduce the affected source files`);
|
|
331
|
+
entryBytes = nextBytes;
|
|
332
|
+
fileBytes.set(path, bytes);
|
|
333
|
+
previousWrites.set(path, current);
|
|
334
|
+
entry.files.set(path, texts);
|
|
335
|
+
this.encoded.delete(entry);
|
|
336
|
+
this.onWrite?.(path, text);
|
|
337
|
+
},
|
|
338
|
+
commit: () => {
|
|
339
|
+
if (finished) throw new Error("edit history transaction already finished");
|
|
340
|
+
if (entry.files.size) {
|
|
341
|
+
this.checkSources(entry, "after");
|
|
342
|
+
this.publish([...this.undoStack, entry], []);
|
|
343
|
+
}
|
|
344
|
+
finished = true;
|
|
345
|
+
},
|
|
346
|
+
rollback: () => {
|
|
347
|
+
if (finished) return;
|
|
348
|
+
this.restore(entry, "after", "before", previousWrites);
|
|
349
|
+
finished = true;
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
state() {
|
|
354
|
+
return {
|
|
355
|
+
canUndo: this.undoStack.length > 0,
|
|
356
|
+
canRedo: this.redoStack.length > 0,
|
|
357
|
+
undoName: this.undoStack.at(-1)?.name ?? null,
|
|
358
|
+
redoName: this.redoStack.at(-1)?.name ?? null
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
undo() {
|
|
362
|
+
return this.step(this.undoStack, this.redoStack, "after", "before");
|
|
363
|
+
}
|
|
364
|
+
redo() {
|
|
365
|
+
return this.step(this.redoStack, this.undoStack, "before", "after");
|
|
366
|
+
}
|
|
367
|
+
step(from, to, expect, write) {
|
|
368
|
+
const entry = from.at(-1);
|
|
369
|
+
if (!entry) return { ok: false, reason: `nothing to ${write === "before" ? "undo" : "redo"}` };
|
|
370
|
+
try {
|
|
371
|
+
this.assertCurrent();
|
|
372
|
+
this.checkSources(entry, expect);
|
|
373
|
+
} catch (error) {
|
|
374
|
+
return { ok: false, reason: message(error) };
|
|
375
|
+
}
|
|
376
|
+
try {
|
|
377
|
+
for (const [path, texts] of entry.files) this.write(path, texts[write]);
|
|
378
|
+
const remaining = from.slice(0, -1), moved = [...to, entry];
|
|
379
|
+
this.publish(write === "before" ? remaining : moved, write === "before" ? moved : remaining);
|
|
380
|
+
} catch (error) {
|
|
381
|
+
try {
|
|
382
|
+
this.restore(entry, write, expect);
|
|
383
|
+
} catch (rollbackError) {
|
|
384
|
+
return { ok: false, reason: `${message(error)}; rollback failed: ${message(rollbackError)}` };
|
|
385
|
+
}
|
|
386
|
+
return { ok: false, reason: message(error) };
|
|
387
|
+
}
|
|
388
|
+
return { ok: true, written: [...entry.files.keys()] };
|
|
389
|
+
}
|
|
390
|
+
checkSources(entry, expect) {
|
|
391
|
+
for (const [path, texts] of entry.files) {
|
|
392
|
+
if (this.readSource(path) !== texts[expect]) throw new Error(`${path} changed outside the editor since ${entry.name}`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
/** A failed write may still contain the original text. Never roll back someone else's version. */
|
|
396
|
+
restore(entry, expect, restore, previousWrites) {
|
|
397
|
+
const pending = [];
|
|
398
|
+
for (const [path, texts] of entry.files) {
|
|
399
|
+
const current = this.readSource(path);
|
|
400
|
+
if (current === texts[restore]) continue;
|
|
401
|
+
if (current !== texts[expect] && current !== previousWrites?.get(path)) throw new Error(`${path} changed outside the editor since ${entry.name}`);
|
|
402
|
+
pending.push([path, texts[restore]]);
|
|
403
|
+
}
|
|
404
|
+
for (const [path, text] of pending) this.write(path, text);
|
|
405
|
+
}
|
|
406
|
+
readSource(path) {
|
|
407
|
+
if (!safePath(path)) throw new Error("unsafe edit history source path");
|
|
408
|
+
let target = this.dir;
|
|
409
|
+
for (const part of path.split("/")) {
|
|
410
|
+
target = join2(target, part);
|
|
411
|
+
try {
|
|
412
|
+
if (lstatSync(target).isSymbolicLink()) throw new Error(`${path} is a symlink; cannot restore edit history`);
|
|
413
|
+
} catch (error) {
|
|
414
|
+
if (missing(error)) throw new Error(`${path} no longer exists`, { cause: error });
|
|
415
|
+
throw error;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return readFileSync2(target, "utf8");
|
|
419
|
+
}
|
|
420
|
+
write(path, text) {
|
|
421
|
+
const target = join2(this.dir, path);
|
|
422
|
+
this.onWrite?.(path, text);
|
|
423
|
+
this.atomicWrite(target, text, lstatSync(target).mode & 511);
|
|
424
|
+
}
|
|
425
|
+
cacheDir() {
|
|
426
|
+
const cache = join2(this.dir, ".higgsedit-cache");
|
|
427
|
+
try {
|
|
428
|
+
const stat = lstatSync(cache);
|
|
429
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("unsafe edit history cache directory");
|
|
430
|
+
} catch (error) {
|
|
431
|
+
if (!missing(error)) throw error;
|
|
432
|
+
}
|
|
433
|
+
return cache;
|
|
434
|
+
}
|
|
435
|
+
readJournal() {
|
|
436
|
+
const file = join2(this.cacheDir(), "history.json");
|
|
437
|
+
let fd;
|
|
438
|
+
try {
|
|
439
|
+
if (!lstatSync(file).isFile()) throw new Error("edit history journal is not a regular file");
|
|
440
|
+
fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
441
|
+
} catch (error) {
|
|
442
|
+
if (missing(error)) return { revision: "missing" };
|
|
443
|
+
throw new Error(`cannot read edit history: ${message(error)}`, { cause: error });
|
|
444
|
+
}
|
|
445
|
+
try {
|
|
446
|
+
const stat = fstatSync(fd, { bigint: true });
|
|
447
|
+
if (!stat.isFile()) throw new Error("edit history journal is not a regular file");
|
|
448
|
+
const revision = identity(stat);
|
|
449
|
+
if (stat.size > BigInt(this.maxBytes)) return { revision };
|
|
450
|
+
const bytes = Buffer.alloc(Number(stat.size) + 1);
|
|
451
|
+
let length = 0, count = 0;
|
|
452
|
+
do {
|
|
453
|
+
count = readSync(fd, bytes, length, bytes.length - length, null);
|
|
454
|
+
length += count;
|
|
455
|
+
} while (count && length < bytes.length);
|
|
456
|
+
if (BigInt(length) !== stat.size || identity(fstatSync(fd, { bigint: true })) !== revision) throw new Error("edit history changed while reading");
|
|
457
|
+
const data = bytes.subarray(0, length);
|
|
458
|
+
return { revision, text: data.toString("utf8") };
|
|
459
|
+
} finally {
|
|
460
|
+
closeSync(fd);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
assertCurrent() {
|
|
464
|
+
let revision;
|
|
465
|
+
try {
|
|
466
|
+
const stat = lstatSync(join2(this.cacheDir(), "history.json"), { bigint: true });
|
|
467
|
+
if (!stat.isFile()) throw new Error("edit history journal is not a regular file");
|
|
468
|
+
revision = identity(stat);
|
|
469
|
+
} catch (error) {
|
|
470
|
+
if (!missing(error)) throw error;
|
|
471
|
+
revision = "missing";
|
|
472
|
+
}
|
|
473
|
+
if (revision !== this.revision) throw new Error(STALE);
|
|
474
|
+
}
|
|
475
|
+
serialized(entry) {
|
|
476
|
+
let saved = this.encoded.get(entry);
|
|
477
|
+
if (!saved) {
|
|
478
|
+
const text = JSON.stringify(entryValue(entry));
|
|
479
|
+
saved = { text, bytes: Buffer.byteLength(text) };
|
|
480
|
+
this.encoded.set(entry, saved);
|
|
481
|
+
}
|
|
482
|
+
return saved;
|
|
483
|
+
}
|
|
484
|
+
bound(undo, redo) {
|
|
485
|
+
const revision = randomUUID();
|
|
486
|
+
const sized = (stack) => stack.map((entry) => ({ entry, bytes: this.serialized(entry).bytes }));
|
|
487
|
+
const keepUndo = sized(undo), keepRedo = sized(redo);
|
|
488
|
+
let bytes = Buffer.byteLength(encode([], [], revision)) + [...keepUndo, ...keepRedo].reduce((sum, entry) => sum + entry.bytes, 0);
|
|
489
|
+
const separators = () => Math.max(0, keepUndo.length - 1) + Math.max(0, keepRedo.length - 1);
|
|
490
|
+
while (keepUndo.length + keepRedo.length > this.maxEntries || bytes + separators() > this.maxBytes) {
|
|
491
|
+
const removed = keepUndo.length > 1 || !keepRedo.length ? keepUndo.shift() : keepRedo.shift();
|
|
492
|
+
if (!removed) throw new Error("edit history size limit exceeded");
|
|
493
|
+
bytes -= removed.bytes;
|
|
494
|
+
}
|
|
495
|
+
undo = keepUndo.map(({ entry }) => entry);
|
|
496
|
+
redo = keepRedo.map(({ entry }) => entry);
|
|
497
|
+
return { undo, redo, text: encode(undo, redo, revision, (entry) => this.serialized(entry).text) };
|
|
498
|
+
}
|
|
499
|
+
publish(undo, redo) {
|
|
500
|
+
const next = this.bound(undo, redo);
|
|
501
|
+
this.assertCurrent();
|
|
502
|
+
const cache = this.cacheDir();
|
|
503
|
+
mkdirSync(cache, { recursive: true, mode: 448 });
|
|
504
|
+
const target = join2(cache, "history.json");
|
|
505
|
+
try {
|
|
506
|
+
this.revision = this.atomicWrite(target, next.text, 384);
|
|
507
|
+
} catch (error) {
|
|
508
|
+
throw new Error(`cannot save edit history: ${message(error)}`, { cause: error });
|
|
509
|
+
}
|
|
510
|
+
this.undoStack = next.undo;
|
|
511
|
+
this.redoStack = next.redo;
|
|
512
|
+
}
|
|
513
|
+
atomicWrite(target, text, mode) {
|
|
514
|
+
const tmp = `${target}.higgsedit-${process.pid}-${randomUUID()}.tmp`;
|
|
515
|
+
let published = false;
|
|
516
|
+
let fd;
|
|
517
|
+
try {
|
|
518
|
+
fd = openSync(tmp, "wx", mode);
|
|
519
|
+
writeFileSync(fd, text);
|
|
520
|
+
fsyncSync(fd);
|
|
521
|
+
renameSync(tmp, target);
|
|
522
|
+
published = true;
|
|
523
|
+
return identity(fstatSync(fd, { bigint: true }));
|
|
524
|
+
} finally {
|
|
525
|
+
try {
|
|
526
|
+
if (fd !== void 0) closeSync(fd);
|
|
527
|
+
} finally {
|
|
528
|
+
if (!published) {
|
|
529
|
+
try {
|
|
530
|
+
unlinkSync(tmp);
|
|
531
|
+
} catch {
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
// src/prerender.ts
|
|
540
|
+
import { createHash } from "node:crypto";
|
|
541
|
+
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
|
+
import { once } from "node:events";
|
|
543
|
+
import { join as join3, posix } from "node:path";
|
|
544
|
+
var PRERENDER_DIR = ".higgsedit/prerender";
|
|
545
|
+
var DEFAULT_CAP_MB = 1024;
|
|
546
|
+
var GOP_SECONDS = 0.5;
|
|
547
|
+
var CLIENT_TTL_MS = 5 * 6e4;
|
|
548
|
+
var PROGRESS_MS = 250;
|
|
549
|
+
var IDLE_MS = 6e4;
|
|
550
|
+
var OUTRUN = "the sources changed since the editor asked";
|
|
551
|
+
var sha = (text) => createHash("sha256").update(text).digest("hex");
|
|
552
|
+
var even = (n) => Math.max(2, Math.round(n / 2) * 2);
|
|
553
|
+
function glob(pattern) {
|
|
554
|
+
return new RegExp(`^${pattern.split("*").map((s) => s.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join("[^/]*")}$`);
|
|
555
|
+
}
|
|
556
|
+
var Prerenderer = class {
|
|
557
|
+
constructor(dir, onEvent = () => {
|
|
558
|
+
}, log = () => {
|
|
559
|
+
}, opts = {}) {
|
|
560
|
+
this.dir = dir;
|
|
561
|
+
this.onEvent = onEvent;
|
|
562
|
+
this.log = log;
|
|
563
|
+
this.root = join3(dir, PRERENDER_DIR);
|
|
564
|
+
this.capBytes = (opts.capMb ?? Number(process.env.HIGGSEDIT_PRERENDER_MB ?? DEFAULT_CAP_MB)) * 1024 * 1024;
|
|
565
|
+
}
|
|
566
|
+
dir;
|
|
567
|
+
onEvent;
|
|
568
|
+
log;
|
|
569
|
+
root;
|
|
570
|
+
wanted = [];
|
|
571
|
+
/** Each editor's last wish list, by the id it sends. */
|
|
572
|
+
clients = /* @__PURE__ */ new Map();
|
|
573
|
+
playhead = 0;
|
|
574
|
+
status = /* @__PURE__ */ new Map();
|
|
575
|
+
current;
|
|
576
|
+
running = false;
|
|
577
|
+
capture;
|
|
578
|
+
loadedSources;
|
|
579
|
+
idle;
|
|
580
|
+
stopped = false;
|
|
581
|
+
renderer;
|
|
582
|
+
capBytes;
|
|
583
|
+
/** What the picture is drawn by: the capture page's code and the pinned Chrome. */
|
|
584
|
+
rendererId() {
|
|
585
|
+
this.renderer ??= pageBundle("capture-page").then((code) => sha(`${CHROME_VERSION}
|
|
586
|
+
${code}`));
|
|
587
|
+
return this.renderer;
|
|
588
|
+
}
|
|
589
|
+
/** Size and mtime of every project file `patterns` name (`*` within one path segment), and of the project's fonts. */
|
|
590
|
+
hostPart(patterns) {
|
|
591
|
+
const lines = [];
|
|
592
|
+
for (const pattern of patterns) {
|
|
593
|
+
const rel2 = posix.normalize(pattern.replace(/^\/+/, ""));
|
|
594
|
+
if (rel2.startsWith("..")) continue;
|
|
595
|
+
const files = rel2.includes("*") ? this.expand(rel2) : [rel2];
|
|
596
|
+
for (const f of files) {
|
|
597
|
+
try {
|
|
598
|
+
const st = statSync2(join3(this.dir, f));
|
|
599
|
+
if (st.isFile()) lines.push(`${f} ${st.size} ${st.mtimeMs}`);
|
|
600
|
+
} catch {
|
|
601
|
+
lines.push(`${f} -`);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
for (const f of fontFiles(this.dir)) {
|
|
606
|
+
const st = statSync2(join3(this.dir, f));
|
|
607
|
+
lines.push(`font ${f} ${st.size} ${st.mtimeMs}`);
|
|
608
|
+
}
|
|
609
|
+
return lines.join("\n");
|
|
610
|
+
}
|
|
611
|
+
expand(pattern) {
|
|
612
|
+
const dir = posix.dirname(pattern);
|
|
613
|
+
if (dir.includes("*")) return [];
|
|
614
|
+
const re = glob(posix.basename(pattern));
|
|
615
|
+
try {
|
|
616
|
+
return readdirSync2(join3(this.dir, dir)).filter((n) => re.test(n)).map((n) => posix.join(dir, n)).toSorted();
|
|
617
|
+
} catch {
|
|
618
|
+
return [];
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
/** The cache id of `target` at its size. */
|
|
622
|
+
async idOf(target) {
|
|
623
|
+
return sha(`${target.digest}
|
|
624
|
+
${this.hostPart(target.media)}
|
|
625
|
+
${await this.rendererId()}
|
|
626
|
+
${target.size}`).slice(0, 32);
|
|
627
|
+
}
|
|
628
|
+
entry(id) {
|
|
629
|
+
try {
|
|
630
|
+
return JSON.parse(readFileSync3(join3(this.root, `${id}.json`), "utf8"));
|
|
631
|
+
} catch {
|
|
632
|
+
return void 0;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
/** The cache file `name` (`<id>.mp4`, …) if it exists; marks it used. */
|
|
636
|
+
lookup(name) {
|
|
637
|
+
if (!/^[0-9a-f]{32}(\.matte)?\.(mp4|json)$/.test(name)) return void 0;
|
|
638
|
+
const file = join3(this.root, name);
|
|
639
|
+
if (!existsSync2(file)) return void 0;
|
|
640
|
+
const meta = join3(this.root, `${name.slice(0, 32)}.json`);
|
|
641
|
+
const now = /* @__PURE__ */ new Date();
|
|
642
|
+
try {
|
|
643
|
+
utimesSync(meta, now, now);
|
|
644
|
+
} catch {
|
|
645
|
+
}
|
|
646
|
+
return file;
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* One editor's current wish list (`client` names the editor: two tabs on one project each keep theirs): what each
|
|
650
|
+
* target's state is now. Targets no editor wants any more leave the queue and a render of one is cancelled; the
|
|
651
|
+
* rest render nearest the latest `playhead` (root seconds) first.
|
|
652
|
+
*/
|
|
653
|
+
async request(targets, playhead = 0, client = "") {
|
|
654
|
+
const ids = await Promise.all(targets.map((t) => this.idOf(t)));
|
|
655
|
+
const now = Date.now();
|
|
656
|
+
this.clients.set(client, { targets: targets.map((t, i) => ({ ...t, id: ids[i] })), at: now });
|
|
657
|
+
for (const [c, w] of this.clients) if (now - w.at > CLIENT_TTL_MS && c !== client) this.clients.delete(c);
|
|
658
|
+
const wanted = /* @__PURE__ */ new Map();
|
|
659
|
+
for (const w of this.clients.values()) for (const t of w.targets) wanted.set(t.id, t);
|
|
660
|
+
this.wanted = [...wanted.values()];
|
|
661
|
+
this.playhead = playhead;
|
|
662
|
+
if (process.env.HIGGSEDIT_PRERENDER_DEBUG) this.log(`prerender request ${client}: ${targets.map((t, i) => `${t.key}=${ids[i].slice(0, 8)}@${t.size}`).join(", ") || "(none)"}`);
|
|
663
|
+
if (this.current && !wanted.has(this.current.id)) {
|
|
664
|
+
this.log(`prerender: cancelled ${this.current.id.slice(0, 8)} (no longer wanted)`);
|
|
665
|
+
this.current.abort.abort();
|
|
666
|
+
}
|
|
667
|
+
for (const id of this.status.keys()) if (!wanted.has(id) && this.current?.id !== id) this.status.delete(id);
|
|
668
|
+
const out = targets.map((t, i) => this.statusOf(t, ids[i]));
|
|
669
|
+
for (const t of this.wanted) this.statusOf(t, t.id);
|
|
670
|
+
this.kick();
|
|
671
|
+
return out;
|
|
672
|
+
}
|
|
673
|
+
statusOf(t, id) {
|
|
674
|
+
const known = this.status.get(id);
|
|
675
|
+
if (known && known.state !== "queued" && !(known.state === "failed" && known.error === OUTRUN)) return known;
|
|
676
|
+
const entry = this.entry(id);
|
|
677
|
+
const s = entry ? { node: t.key, id, state: entry.inexact ? "inexact" : "ready", done: entry.frames, total: entry.frames } : { node: t.key, id, state: "queued", done: 0, total: t.frames };
|
|
678
|
+
if (entry) this.lookup(`${id}.json`);
|
|
679
|
+
this.status.set(id, s);
|
|
680
|
+
return s;
|
|
681
|
+
}
|
|
682
|
+
/** Statuses of what was last asked for. */
|
|
683
|
+
statuses() {
|
|
684
|
+
return [...this.status.values()];
|
|
685
|
+
}
|
|
686
|
+
next() {
|
|
687
|
+
const dist = (t) => {
|
|
688
|
+
const start = t.rootStart + t.offset;
|
|
689
|
+
const end = start + t.frames / t.fps;
|
|
690
|
+
return this.playhead < start ? start - this.playhead : this.playhead > end ? this.playhead - end : 0;
|
|
691
|
+
};
|
|
692
|
+
return this.wanted.filter((t) => this.status.get(t.id)?.state === "queued").toSorted((a, b) => dist(a) - dist(b))[0];
|
|
693
|
+
}
|
|
694
|
+
kick() {
|
|
695
|
+
if (this.running || this.stopped) return;
|
|
696
|
+
this.running = true;
|
|
697
|
+
void this.loop().finally(() => {
|
|
698
|
+
this.running = false;
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
async loop() {
|
|
702
|
+
for (let job = this.next(); job && !this.stopped; job = this.next()) {
|
|
703
|
+
if (this.idle) clearTimeout(this.idle);
|
|
704
|
+
const abort = new AbortController();
|
|
705
|
+
this.current = { id: job.id, abort };
|
|
706
|
+
const s = this.status.get(job.id);
|
|
707
|
+
s.state = "rendering";
|
|
708
|
+
this.onEvent({ ...s });
|
|
709
|
+
try {
|
|
710
|
+
const entry = await this.render(job, abort.signal, (done) => {
|
|
711
|
+
s.done = done;
|
|
712
|
+
});
|
|
713
|
+
s.state = entry.inexact ? "inexact" : "ready";
|
|
714
|
+
s.done = s.total = entry.frames;
|
|
715
|
+
this.log(`prerender ${job.key}: ${entry.frames} frames ${entry.width}x${entry.height} in ${(entry.renderMs / 1e3).toFixed(1)} s, ${(entry.bytes / 1e6).toFixed(1)} MB${entry.inexact ? " (inexact: blend over transparency)" : ""}`);
|
|
716
|
+
this.evict();
|
|
717
|
+
} catch (error) {
|
|
718
|
+
const message2 = error instanceof Error ? error.message : String(error);
|
|
719
|
+
if (abort.signal.aborted) this.status.delete(job.id);
|
|
720
|
+
else {
|
|
721
|
+
s.state = "failed";
|
|
722
|
+
s.error = message2;
|
|
723
|
+
this.log(`prerender ${job.key}: ${message2}`);
|
|
724
|
+
}
|
|
725
|
+
} finally {
|
|
726
|
+
this.current = void 0;
|
|
727
|
+
}
|
|
728
|
+
if (this.status.has(job.id)) this.onEvent({ ...s });
|
|
729
|
+
}
|
|
730
|
+
this.idle = setTimeout(() => void this.closeCapture(), IDLE_MS);
|
|
731
|
+
}
|
|
732
|
+
async session() {
|
|
733
|
+
this.capture ??= openCapture(this.dir, 8);
|
|
734
|
+
const session = await this.capture;
|
|
735
|
+
const sources = JSON.stringify(readSources(this.dir)) + JSON.stringify(fontFiles(this.dir));
|
|
736
|
+
if (this.loadedSources === void 0) this.loadedSources = sources;
|
|
737
|
+
else if (this.loadedSources !== sources) {
|
|
738
|
+
await session.reload();
|
|
739
|
+
this.loadedSources = sources;
|
|
740
|
+
}
|
|
741
|
+
return session;
|
|
742
|
+
}
|
|
743
|
+
async closeCapture() {
|
|
744
|
+
const capture = this.capture;
|
|
745
|
+
this.capture = void 0;
|
|
746
|
+
this.loadedSources = void 0;
|
|
747
|
+
await (await capture?.catch(() => void 0))?.close();
|
|
748
|
+
}
|
|
749
|
+
async render(job, signal, progress) {
|
|
750
|
+
const started = performance.now();
|
|
751
|
+
const session = await this.session();
|
|
752
|
+
const page = pageApi(session.page);
|
|
753
|
+
const input = (await page.inputs()).find((i) => i.key === job.key);
|
|
754
|
+
if (!input) throw new Error(`${job.key} is not something to pre-render in the current sources`);
|
|
755
|
+
const id = await this.idOf({ ...input, size: job.size });
|
|
756
|
+
if (id !== job.id) throw new Error(OUTRUN);
|
|
757
|
+
const height = even(job.size);
|
|
758
|
+
const width = even(input.width * height / input.height);
|
|
759
|
+
mkdirSync2(this.root, { recursive: true });
|
|
760
|
+
const ignore = join3(this.root, "..", ".gitignore");
|
|
761
|
+
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`);
|
|
764
|
+
const gop = String(Math.max(1, Math.round(input.fps * GOP_SECONDS)));
|
|
765
|
+
const tags = ["-colorspace", "bt709", "-color_primaries", "bt709", "-color_trc", "bt709"];
|
|
766
|
+
const encoder = ffmpeg(
|
|
767
|
+
[
|
|
768
|
+
"-y",
|
|
769
|
+
"-f",
|
|
770
|
+
"rawvideo",
|
|
771
|
+
"-pix_fmt",
|
|
772
|
+
"rgba",
|
|
773
|
+
"-s",
|
|
774
|
+
`${width}x${height}`,
|
|
775
|
+
"-framerate",
|
|
776
|
+
String(input.fps),
|
|
777
|
+
"-i",
|
|
778
|
+
"pipe:0",
|
|
779
|
+
"-filter_complex",
|
|
780
|
+
"[0:v]split[c][a];[c]scale=out_color_matrix=bt709:out_range=limited:flags=accurate_rnd+full_chroma_int,format=yuv420p,setparams=color_primaries=bt709:color_trc=bt709:colorspace=bt709:range=tv[v];[a]alphaextract,format=gray,scale=in_range=full:out_range=full,format=yuv420p[m]",
|
|
781
|
+
"-map",
|
|
782
|
+
"[v]",
|
|
783
|
+
"-c:v",
|
|
784
|
+
"libx264",
|
|
785
|
+
"-preset",
|
|
786
|
+
"veryfast",
|
|
787
|
+
"-crf",
|
|
788
|
+
"16",
|
|
789
|
+
"-bf",
|
|
790
|
+
"0",
|
|
791
|
+
"-g",
|
|
792
|
+
gop,
|
|
793
|
+
...tags,
|
|
794
|
+
"-color_range",
|
|
795
|
+
"tv",
|
|
796
|
+
"-movflags",
|
|
797
|
+
"+faststart",
|
|
798
|
+
"-an",
|
|
799
|
+
colour,
|
|
800
|
+
"-map",
|
|
801
|
+
"[m]",
|
|
802
|
+
"-c:v",
|
|
803
|
+
"libx264",
|
|
804
|
+
"-preset",
|
|
805
|
+
"veryfast",
|
|
806
|
+
"-crf",
|
|
807
|
+
"4",
|
|
808
|
+
"-bf",
|
|
809
|
+
"0",
|
|
810
|
+
"-g",
|
|
811
|
+
gop,
|
|
812
|
+
...tags,
|
|
813
|
+
"-color_range",
|
|
814
|
+
"pc",
|
|
815
|
+
"-movflags",
|
|
816
|
+
"+faststart",
|
|
817
|
+
"-an",
|
|
818
|
+
matte
|
|
819
|
+
],
|
|
820
|
+
"pipe"
|
|
821
|
+
);
|
|
822
|
+
let stderr = "";
|
|
823
|
+
encoder.stderr.on("data", (d) => stderr += d.toString());
|
|
824
|
+
const exited = once(encoder, "close");
|
|
825
|
+
const stdin = encoder.stdin;
|
|
826
|
+
const bytes = width * height * 4;
|
|
827
|
+
const sink = session.sink(async (_n, body) => {
|
|
828
|
+
if (body.length !== bytes) throw new Error(`frame body is ${body.length} bytes, expected ${bytes}`);
|
|
829
|
+
if (!stdin.write(body)) await once(stdin, "drain");
|
|
830
|
+
});
|
|
831
|
+
let opaque = true;
|
|
832
|
+
let lastEvent = 0;
|
|
833
|
+
const s = this.status.get(job.id);
|
|
834
|
+
try {
|
|
835
|
+
for (let i = 0; i < input.frames; i++) {
|
|
836
|
+
if (signal.aborted || this.stopped) throw new Error("cancelled");
|
|
837
|
+
const r = await page.raw(job.key, input.firstFrame + i, height, `${sink}/${i}`);
|
|
838
|
+
opaque &&= r.opaque;
|
|
839
|
+
progress(i + 1);
|
|
840
|
+
if (s && performance.now() - lastEvent > PROGRESS_MS) {
|
|
841
|
+
lastEvent = performance.now();
|
|
842
|
+
this.onEvent({ ...s });
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
const misses = await api(session.page).fontMisses();
|
|
846
|
+
if (misses.length) throw new Error(`fonts did not load (${misses.map((m) => m.family).join(", ")}); not caching a fallback`);
|
|
847
|
+
} catch (error) {
|
|
848
|
+
stdin.destroy();
|
|
849
|
+
encoder.kill("SIGKILL");
|
|
850
|
+
await exited;
|
|
851
|
+
rmSync(colour, { force: true });
|
|
852
|
+
rmSync(matte, { force: true });
|
|
853
|
+
throw error;
|
|
854
|
+
}
|
|
855
|
+
stdin.end();
|
|
856
|
+
const [code] = await exited;
|
|
857
|
+
if (code !== 0) {
|
|
858
|
+
rmSync(colour, { force: true });
|
|
859
|
+
rmSync(matte, { force: true });
|
|
860
|
+
throw new Error(`ffmpeg exited with ${code}: ${stderr.trim()}`);
|
|
861
|
+
}
|
|
862
|
+
const inexact = input.blend && !opaque;
|
|
863
|
+
const entry = { node: job.key, width, height, fps: input.fps, frames: input.frames, offset: input.offset, opaque, inexact, renderMs: Math.round(performance.now() - started), bytes: 0 };
|
|
864
|
+
if (inexact) {
|
|
865
|
+
rmSync(colour, { force: true });
|
|
866
|
+
rmSync(matte, { force: true });
|
|
867
|
+
} else {
|
|
868
|
+
renameSync2(colour, join3(this.root, `${id}.mp4`));
|
|
869
|
+
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);
|
|
872
|
+
}
|
|
873
|
+
writeFileSync2(join3(this.root, `${id}.json.part`), JSON.stringify(entry));
|
|
874
|
+
renameSync2(join3(this.root, `${id}.json.part`), join3(this.root, `${id}.json`));
|
|
875
|
+
return entry;
|
|
876
|
+
}
|
|
877
|
+
/** Drops the least recently used pictures until the folder fits its cap; never one the editor asked for last. */
|
|
878
|
+
evict() {
|
|
879
|
+
let names;
|
|
880
|
+
try {
|
|
881
|
+
names = readdirSync2(this.root);
|
|
882
|
+
} catch {
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
const wanted = new Set(this.wanted.map((t) => t.id));
|
|
886
|
+
const groups = /* @__PURE__ */ new Map();
|
|
887
|
+
for (const n of names) {
|
|
888
|
+
const id = n.slice(0, 32);
|
|
889
|
+
if (!/^[0-9a-f]{32}/.test(id) || n.includes(".part")) continue;
|
|
890
|
+
const st = statSync2(join3(this.root, n));
|
|
891
|
+
const g = groups.get(id) ?? { size: 0, used: 0 };
|
|
892
|
+
g.size += st.size;
|
|
893
|
+
if (n.endsWith(".json")) g.used = st.mtimeMs;
|
|
894
|
+
groups.set(id, g);
|
|
895
|
+
}
|
|
896
|
+
let total = [...groups.values()].reduce((a, g) => a + g.size, 0);
|
|
897
|
+
for (const [id, g] of [...groups].toSorted((a, b) => a[1].used - b[1].used)) {
|
|
898
|
+
if (total <= this.capBytes) break;
|
|
899
|
+
if (wanted.has(id)) continue;
|
|
900
|
+
for (const ext of [".json", ".mp4", ".matte.mp4"]) rmSync(join3(this.root, `${id}${ext}`), { force: true });
|
|
901
|
+
total -= g.size;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
async stop() {
|
|
905
|
+
this.stopped = true;
|
|
906
|
+
this.current?.abort.abort();
|
|
907
|
+
if (this.idle) clearTimeout(this.idle);
|
|
908
|
+
await this.closeCapture();
|
|
909
|
+
}
|
|
910
|
+
/** The inputs the capture page computes for the current sources (the CLI and tests stand in for the editor with these). */
|
|
911
|
+
async inputs() {
|
|
912
|
+
return pageApi((await this.session()).page).inputs();
|
|
913
|
+
}
|
|
914
|
+
};
|
|
915
|
+
var pageApi = (page) => ({
|
|
916
|
+
inputs: () => page.evaluate(() => globalThis.higgsedit.prerenderInputs()),
|
|
917
|
+
raw: (key, frame, height, url) => page.evaluate(({ key: key2, frame: frame2, height: height2, url: url2 }) => globalThis.higgsedit.prerenderRaw(key2, frame2, height2, url2), { key, frame, height, url })
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
// src/proxy.ts
|
|
921
|
+
import { execFile } from "node:child_process";
|
|
922
|
+
import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
|
|
923
|
+
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";
|
|
925
|
+
import { promisify } from "node:util";
|
|
926
|
+
var run = promisify(execFile);
|
|
927
|
+
var VIDEO = /\.(mp4|mov|m4v|mkv|webm)$/i;
|
|
928
|
+
var SKIP = /(^|\/)(node_modules|\.git|\.higgsedit-cache|\.higgsedit|renders|dist)(\/|$)/;
|
|
929
|
+
var LONG_SIDE = 1920;
|
|
930
|
+
var DECODABLE = /* @__PURE__ */ new Set(["h264", "hevc", "vp9", "av1"]);
|
|
931
|
+
var needsProxy = (p) => p.width * p.height > LONG_SIDE * 1088 || !DECODABLE.has(p.codec);
|
|
932
|
+
var even2 = (n) => Math.max(2, Math.round(n / 2) * 2);
|
|
933
|
+
var QUIET_MS = 400;
|
|
934
|
+
var TRACKED_LIMIT = 1024;
|
|
935
|
+
var Proxies = class {
|
|
936
|
+
constructor(dir, onReady, log = () => {
|
|
937
|
+
}) {
|
|
938
|
+
this.dir = dir;
|
|
939
|
+
this.onReady = onReady;
|
|
940
|
+
this.log = log;
|
|
941
|
+
this.root = join4(dir, ".higgsedit-cache", "proxies");
|
|
942
|
+
}
|
|
943
|
+
dir;
|
|
944
|
+
onReady;
|
|
945
|
+
log;
|
|
946
|
+
root;
|
|
947
|
+
// Recent actual preview requests bound both pending work and cached no-proxy decisions.
|
|
948
|
+
candidates = /* @__PURE__ */ new Map();
|
|
949
|
+
active;
|
|
950
|
+
timer;
|
|
951
|
+
abort = new AbortController();
|
|
952
|
+
current;
|
|
953
|
+
stopped = false;
|
|
954
|
+
version(rel2) {
|
|
955
|
+
try {
|
|
956
|
+
const st = statSync3(join4(this.dir, rel2));
|
|
957
|
+
if (!st.isFile()) return void 0;
|
|
958
|
+
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
|
+
} catch {
|
|
960
|
+
return void 0;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
/** The finished proxy for a project-relative video path, if there is one. */
|
|
964
|
+
lookup(rel2) {
|
|
965
|
+
const key = this.version(rel2)?.key;
|
|
966
|
+
const file = key && join4(this.root, `${key}.mp4`);
|
|
967
|
+
return file && existsSync3(file) ? file : void 0;
|
|
968
|
+
}
|
|
969
|
+
/** Only /preview/ demand starts work; watcher events refresh previously requested footage. */
|
|
970
|
+
consider(rel2, requested = false) {
|
|
971
|
+
if (this.stopped || !VIDEO.test(rel2) || SKIP.test(rel2)) return;
|
|
972
|
+
const prior = this.candidates.get(rel2);
|
|
973
|
+
if (!requested && !prior) return;
|
|
974
|
+
const version = this.version(rel2);
|
|
975
|
+
if (!version) {
|
|
976
|
+
this.candidates.delete(rel2);
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
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`)) };
|
|
981
|
+
if (requested && candidate.retryAt !== void 0 && now >= candidate.retryAt) {
|
|
982
|
+
candidate.done = false;
|
|
983
|
+
candidate.retryAt = void 0;
|
|
984
|
+
}
|
|
985
|
+
if (requested) this.candidates.delete(rel2);
|
|
986
|
+
this.candidates.set(rel2, candidate);
|
|
987
|
+
while (this.candidates.size > TRACKED_LIMIT) {
|
|
988
|
+
for (const path of this.candidates.keys()) {
|
|
989
|
+
if (path !== this.active) {
|
|
990
|
+
this.candidates.delete(path);
|
|
991
|
+
break;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
this.schedule();
|
|
996
|
+
}
|
|
997
|
+
schedule() {
|
|
998
|
+
clearTimeout(this.timer);
|
|
999
|
+
this.timer = void 0;
|
|
1000
|
+
if (this.stopped || this.active) return;
|
|
1001
|
+
let next = Infinity;
|
|
1002
|
+
for (const [rel2, candidate] of this.candidates) {
|
|
1003
|
+
if (candidate.done) continue;
|
|
1004
|
+
if (candidate.readyAt > Date.now()) {
|
|
1005
|
+
next = Math.min(next, candidate.readyAt);
|
|
1006
|
+
continue;
|
|
1007
|
+
}
|
|
1008
|
+
const version = this.version(rel2);
|
|
1009
|
+
if (!version) {
|
|
1010
|
+
this.candidates.delete(rel2);
|
|
1011
|
+
continue;
|
|
1012
|
+
}
|
|
1013
|
+
if (version.key !== candidate.key) {
|
|
1014
|
+
Object.assign(candidate, version, { readyAt: Date.now() + QUIET_MS });
|
|
1015
|
+
next = Math.min(next, candidate.readyAt);
|
|
1016
|
+
continue;
|
|
1017
|
+
}
|
|
1018
|
+
this.active = rel2;
|
|
1019
|
+
void this.make(rel2, candidate.key).then(() => {
|
|
1020
|
+
if (this.candidates.get(rel2)?.key === candidate.key) candidate.done = true;
|
|
1021
|
+
}, (error) => {
|
|
1022
|
+
if (this.stopped) return;
|
|
1023
|
+
candidate.done = true;
|
|
1024
|
+
candidate.retryAt = Date.now() + 5e3;
|
|
1025
|
+
this.log(`proxy for ${rel2} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1026
|
+
}).finally(() => {
|
|
1027
|
+
this.active = void 0;
|
|
1028
|
+
this.consider(rel2);
|
|
1029
|
+
this.schedule();
|
|
1030
|
+
});
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
if (Number.isFinite(next)) this.timer = setTimeout(() => this.schedule(), Math.max(0, next - Date.now()));
|
|
1034
|
+
}
|
|
1035
|
+
async make(rel2, key) {
|
|
1036
|
+
const file = join4(this.dir, rel2);
|
|
1037
|
+
if (this.lookup(rel2)) return;
|
|
1038
|
+
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
|
+
if (this.stopped || this.version(rel2)?.key !== key) return;
|
|
1040
|
+
const data = JSON.parse(stdout);
|
|
1041
|
+
const stream = data?.streams?.[0];
|
|
1042
|
+
if (!stream || typeof stream.width !== "number" || stream.width <= 0 || typeof stream.height !== "number" || stream.height <= 0 || typeof stream.codec_name !== "string") throw new Error("invalid video stream metadata");
|
|
1043
|
+
const facts = { width: stream.width, height: stream.height, codec: stream.codec_name };
|
|
1044
|
+
if (!needsProxy(facts)) return;
|
|
1045
|
+
const timescale = Number(String(stream.time_base).split("/")[1]) || 9e4;
|
|
1046
|
+
const scale = LONG_SIDE / Math.max(facts.width, facts.height);
|
|
1047
|
+
const [w, h] = scale < 1 ? [even2(facts.width * scale), even2(facts.height * scale)] : [even2(facts.width), even2(facts.height)];
|
|
1048
|
+
mkdirSync3(this.root, { recursive: true });
|
|
1049
|
+
const out = join4(this.root, `${key}.mp4`);
|
|
1050
|
+
const part = join4(this.root, `${key}.${randomUUID2()}.part.mp4`);
|
|
1051
|
+
const t0 = performance.now();
|
|
1052
|
+
this.log(`making a preview proxy for ${rel2} (${facts.width}\xD7${facts.height} \u2192 ${w}\xD7${h})\u2026`);
|
|
1053
|
+
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"];
|
|
1054
|
+
args.push("-fps_mode", "passthrough", "-video_track_timescale", String(timescale), "-c:a", "aac", "-b:a", "160k", "-movflags", "+faststart", part);
|
|
1055
|
+
try {
|
|
1056
|
+
await new Promise((resolve, reject) => {
|
|
1057
|
+
const p = ffmpeg(args);
|
|
1058
|
+
this.current = p;
|
|
1059
|
+
let err = "";
|
|
1060
|
+
p.stderr.on("data", (d) => {
|
|
1061
|
+
err = (err + d.toString()).slice(-16384);
|
|
1062
|
+
});
|
|
1063
|
+
p.on("error", reject);
|
|
1064
|
+
p.on("close", (code) => {
|
|
1065
|
+
if (this.current === p) this.current = void 0;
|
|
1066
|
+
if (code === 0) resolve();
|
|
1067
|
+
else reject(new Error(err.trim() || `ffmpeg exited with ${code}`));
|
|
1068
|
+
});
|
|
1069
|
+
});
|
|
1070
|
+
if (this.stopped || this.version(rel2)?.key !== key) return;
|
|
1071
|
+
renameSync3(part, out);
|
|
1072
|
+
this.log(`proxy for ${rel2} ready in ${((performance.now() - t0) / 1e3).toFixed(1)} s`);
|
|
1073
|
+
this.onReady(`./${rel2}`);
|
|
1074
|
+
} finally {
|
|
1075
|
+
rmSync2(part, { force: true });
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
stop() {
|
|
1079
|
+
this.stopped = true;
|
|
1080
|
+
clearTimeout(this.timer);
|
|
1081
|
+
this.candidates.clear();
|
|
1082
|
+
this.abort.abort();
|
|
1083
|
+
this.current?.kill("SIGKILL");
|
|
1084
|
+
}
|
|
1085
|
+
};
|
|
1086
|
+
|
|
1087
|
+
// src/dev.ts
|
|
1088
|
+
var escapeHtml = (s) => s.replace(/[&<>"]/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
1089
|
+
var EDITOR_HTML = (name, entry) => `<!doctype html>
|
|
1090
|
+
<html lang="en">
|
|
1091
|
+
<head>
|
|
1092
|
+
<meta charset="utf-8">
|
|
1093
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1094
|
+
<meta name="color-scheme" content="dark">
|
|
1095
|
+
<title>higgsedit \xB7 ${escapeHtml(name)}</title>
|
|
1096
|
+
${entry.styles.map((s) => `<link rel="stylesheet" href="/__higgsedit/editor/${s}">`).join("\n")}
|
|
1097
|
+
<script>window.higgseditProject = ${JSON.stringify({ name }).replace(/</g, "\\u003c")};</script>
|
|
1098
|
+
</head>
|
|
1099
|
+
<body>
|
|
1100
|
+
<div id="app"></div>
|
|
1101
|
+
<script type="module" src="/__higgsedit/editor/${entry.script}"></script>
|
|
1102
|
+
</body>
|
|
1103
|
+
</html>`;
|
|
1104
|
+
var seconds = (v, what) => {
|
|
1105
|
+
if (v === void 0 || v === null) return void 0;
|
|
1106
|
+
if (typeof v !== "number" || !(v >= 0)) throw new SelectError(`${what} must be source seconds, not ${JSON.stringify(v)}`, 400);
|
|
1107
|
+
return v;
|
|
1108
|
+
};
|
|
1109
|
+
function selectRequest(dir, raw) {
|
|
1110
|
+
let body;
|
|
1111
|
+
try {
|
|
1112
|
+
body = JSON.parse(raw || "{}");
|
|
1113
|
+
} catch {
|
|
1114
|
+
throw new SelectError("the body must be JSON", 400);
|
|
1115
|
+
}
|
|
1116
|
+
const src = typeof body.src === "string" ? body.src : "";
|
|
1117
|
+
const file = inside(dir, src);
|
|
1118
|
+
if (!file) throw new SelectError(`${src} is outside the project`, 403);
|
|
1119
|
+
if (!src || !existsSync4(file)) throw new SelectError(`${src || "src"}: no such file in the project`, 404);
|
|
1120
|
+
const pick = body.pick;
|
|
1121
|
+
if (pick !== void 0 && pick !== null && !(Array.isArray(pick) && pick.length === 4 && pick.every((v) => typeof v === "number" && v >= 0 && v <= 1))) {
|
|
1122
|
+
throw new SelectError("pick must be the object's box [x0, y0, x1, y1] in 0..1", 400);
|
|
1123
|
+
}
|
|
1124
|
+
const start = seconds(body.start, "start");
|
|
1125
|
+
const end = seconds(body.end, "end");
|
|
1126
|
+
return {
|
|
1127
|
+
file,
|
|
1128
|
+
stem: basename(file, extname(file)),
|
|
1129
|
+
time: seconds(body.time, "time") ?? 0,
|
|
1130
|
+
prompt: parsePrompt(body.prompt),
|
|
1131
|
+
...Array.isArray(pick) ? { pick } : {},
|
|
1132
|
+
...start !== void 0 ? { start } : {},
|
|
1133
|
+
...end !== void 0 ? { end } : {}
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
async function startDev(dir, port = 0, options = {}) {
|
|
1137
|
+
const presets = presetsDir(options.presets);
|
|
1138
|
+
const mediaUploads = new MediaUploads(dir, options.mediaUploads);
|
|
1139
|
+
const clients = /* @__PURE__ */ new Set();
|
|
1140
|
+
const known = /* @__PURE__ */ new Map();
|
|
1141
|
+
for (const f of sourceFiles(dir)) known.set(f, readFileSync4(join5(dir, f), "utf8"));
|
|
1142
|
+
const ownWrites = /* @__PURE__ */ new Map();
|
|
1143
|
+
const history = new EditHistory(dir, (path, text) => ownWrites.set(path, text));
|
|
1144
|
+
const broadcast = (event) => {
|
|
1145
|
+
const data = `data: ${JSON.stringify(event)}
|
|
1146
|
+
|
|
1147
|
+
`;
|
|
1148
|
+
for (const c of clients) c.write(data);
|
|
1149
|
+
};
|
|
1150
|
+
let timer;
|
|
1151
|
+
let firstSeen = 0;
|
|
1152
|
+
const flush = () => {
|
|
1153
|
+
timer = void 0;
|
|
1154
|
+
const changed = [];
|
|
1155
|
+
let origin = "external";
|
|
1156
|
+
let mtime = 0;
|
|
1157
|
+
let present;
|
|
1158
|
+
try {
|
|
1159
|
+
present = sourceFiles(dir);
|
|
1160
|
+
} catch {
|
|
1161
|
+
present = [];
|
|
1162
|
+
}
|
|
1163
|
+
for (const f of /* @__PURE__ */ new Set([...present, ...known.keys()])) {
|
|
1164
|
+
let text;
|
|
1165
|
+
try {
|
|
1166
|
+
text = readFileSync4(join5(dir, f), "utf8");
|
|
1167
|
+
mtime = Math.max(mtime, statSync4(join5(dir, f)).mtimeMs);
|
|
1168
|
+
} catch {
|
|
1169
|
+
text = void 0;
|
|
1170
|
+
}
|
|
1171
|
+
if (text === known.get(f)) continue;
|
|
1172
|
+
if (text === void 0) known.delete(f);
|
|
1173
|
+
else known.set(f, text);
|
|
1174
|
+
if (ownWrites.get(f) === text) {
|
|
1175
|
+
origin = "editor";
|
|
1176
|
+
ownWrites.delete(f);
|
|
1177
|
+
}
|
|
1178
|
+
changed.push(f);
|
|
1179
|
+
}
|
|
1180
|
+
if (changed.length) broadcast({ type: "change", files: changed, origin, detectedAt: firstSeen, mtime, undo: history.state() });
|
|
1181
|
+
};
|
|
1182
|
+
const proxies = new Proxies(dir, (src) => broadcast({ type: "proxy", src }), (line) => console.log(line));
|
|
1183
|
+
const prerender = new Prerenderer(dir, (status) => broadcast({ type: "prerender", ...status }), (line) => console.log(line));
|
|
1184
|
+
const watcher = watch(dir, { recursive: true }, (_event, name) => {
|
|
1185
|
+
const file = (name ?? "").split("\\").join("/");
|
|
1186
|
+
if (/(^|\/)(node_modules|\.git|\.higgsedit-cache|\.higgsedit|renders)(\/|$)/.test(file)) return;
|
|
1187
|
+
if (file.split("/").some((part) => part.startsWith(MEDIA_UPLOAD_TEMP_PREFIX))) return;
|
|
1188
|
+
proxies.consider(file);
|
|
1189
|
+
if (file === "media" || file.startsWith("media/")) return;
|
|
1190
|
+
if (!timer) firstSeen = Date.now();
|
|
1191
|
+
if (!timer) timer = setTimeout(flush, 15);
|
|
1192
|
+
});
|
|
1193
|
+
const jobs = /* @__PURE__ */ new Map();
|
|
1194
|
+
let jobSeq = 0;
|
|
1195
|
+
const tracks = /* @__PURE__ */ new Map();
|
|
1196
|
+
let stopping = false;
|
|
1197
|
+
let mutations = 0;
|
|
1198
|
+
let stopped;
|
|
1199
|
+
const busy = () => mediaUploads.busy || mutations > 0 || [...jobs.values()].some((job) => job.state === "running") || [...tracks.values()].some((job) => job.state.status === "running");
|
|
1200
|
+
const server = await startServer(
|
|
1201
|
+
dir,
|
|
1202
|
+
[
|
|
1203
|
+
(req, res, url) => {
|
|
1204
|
+
if (stopping) {
|
|
1205
|
+
res.setHeader("retry-after", "1");
|
|
1206
|
+
json(res, 503, { error: "editor is stopping" });
|
|
1207
|
+
return true;
|
|
1208
|
+
}
|
|
1209
|
+
if (url.pathname === "/api/idle-stop" && req.method === "POST") {
|
|
1210
|
+
if (busy()) {
|
|
1211
|
+
json(res, 409, { busy: true });
|
|
1212
|
+
return true;
|
|
1213
|
+
}
|
|
1214
|
+
stopping = true;
|
|
1215
|
+
res.once("finish", () => {
|
|
1216
|
+
options.onIdleStop?.();
|
|
1217
|
+
void stop();
|
|
1218
|
+
});
|
|
1219
|
+
json(res, 200, { stopping: true });
|
|
1220
|
+
return true;
|
|
1221
|
+
}
|
|
1222
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
1223
|
+
mutations++;
|
|
1224
|
+
let released = false;
|
|
1225
|
+
const release = () => {
|
|
1226
|
+
if (!released) {
|
|
1227
|
+
released = true;
|
|
1228
|
+
mutations--;
|
|
1229
|
+
}
|
|
1230
|
+
};
|
|
1231
|
+
res.once("finish", release);
|
|
1232
|
+
res.once("close", release);
|
|
1233
|
+
}
|
|
1234
|
+
return false;
|
|
1235
|
+
},
|
|
1236
|
+
(req, res, url) => mediaUploads.handle(req, res, url),
|
|
1237
|
+
(req, res, url) => {
|
|
1238
|
+
if (url.pathname !== "/api/activity" || req.method !== "GET") return false;
|
|
1239
|
+
json(res, 200, { busy: busy() });
|
|
1240
|
+
return true;
|
|
1241
|
+
},
|
|
1242
|
+
async (req, res, url) => {
|
|
1243
|
+
if (url.pathname !== "/" && url.pathname !== "/index.html") return false;
|
|
1244
|
+
let entry;
|
|
1245
|
+
try {
|
|
1246
|
+
entry = await editorEntry((line) => console.log(line));
|
|
1247
|
+
} catch (error) {
|
|
1248
|
+
if (!(error instanceof MissingEditorError)) throw error;
|
|
1249
|
+
res.statusCode = 404;
|
|
1250
|
+
res.end(error.message);
|
|
1251
|
+
return true;
|
|
1252
|
+
}
|
|
1253
|
+
res.setHeader("content-type", "text/html; charset=utf-8");
|
|
1254
|
+
res.setHeader("cache-control", "no-cache");
|
|
1255
|
+
res.end(EDITOR_HTML(dir.split("/").at(-1) ?? "project", entry));
|
|
1256
|
+
void req;
|
|
1257
|
+
return true;
|
|
1258
|
+
},
|
|
1259
|
+
(req, res, url) => {
|
|
1260
|
+
if (!url.pathname.startsWith("/__higgsedit/editor/")) return false;
|
|
1261
|
+
const rel2 = decodeURIComponent(url.pathname.slice("/__higgsedit/editor/".length));
|
|
1262
|
+
if (rel2.split("/").some((p) => p === ".." || p.startsWith("."))) {
|
|
1263
|
+
res.statusCode = 403;
|
|
1264
|
+
res.end();
|
|
1265
|
+
return true;
|
|
1266
|
+
}
|
|
1267
|
+
sendFile(req, res, editorFile(rel2));
|
|
1268
|
+
return true;
|
|
1269
|
+
},
|
|
1270
|
+
(req, res, url) => {
|
|
1271
|
+
if (url.pathname !== "/api/events") return false;
|
|
1272
|
+
res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache", connection: "keep-alive" });
|
|
1273
|
+
res.write(`data: ${JSON.stringify({ type: "hello" })}
|
|
1274
|
+
|
|
1275
|
+
`);
|
|
1276
|
+
clients.add(res);
|
|
1277
|
+
req.on("close", () => clients.delete(res));
|
|
1278
|
+
return true;
|
|
1279
|
+
},
|
|
1280
|
+
async (req, res, url) => {
|
|
1281
|
+
if (url.pathname !== "/api/edit" || req.method !== "POST") return false;
|
|
1282
|
+
const body = JSON.parse((await readBody(req)).toString());
|
|
1283
|
+
const tx = history.begin(body.name ?? "Edit");
|
|
1284
|
+
try {
|
|
1285
|
+
const results = applyEdits(dir, body.edits, { onWrite: tx.onWrite, ...body.dryRun ? { dryRun: true } : {} });
|
|
1286
|
+
if (results.every((r) => r.ok)) tx.commit();
|
|
1287
|
+
else tx.rollback();
|
|
1288
|
+
json(res, 200, { ok: results.every((r) => r.ok), results, undo: history.state() });
|
|
1289
|
+
} catch (error) {
|
|
1290
|
+
tx.rollback();
|
|
1291
|
+
throw error;
|
|
1292
|
+
}
|
|
1293
|
+
return true;
|
|
1294
|
+
},
|
|
1295
|
+
(req, res, url) => {
|
|
1296
|
+
if (url.pathname !== "/api/undo" && url.pathname !== "/api/redo" || req.method !== "POST") return false;
|
|
1297
|
+
const r = url.pathname === "/api/undo" ? history.undo() : history.redo();
|
|
1298
|
+
json(res, 200, { ...r, undo: history.state() });
|
|
1299
|
+
return true;
|
|
1300
|
+
},
|
|
1301
|
+
async (req, res, url) => {
|
|
1302
|
+
if (!url.pathname.startsWith("/api/render")) return false;
|
|
1303
|
+
if (url.pathname === "/api/render/capabilities" && req.method === "GET") {
|
|
1304
|
+
try {
|
|
1305
|
+
json(res, 200, await nativeExportCapabilities());
|
|
1306
|
+
} catch (error) {
|
|
1307
|
+
json(res, 503, { error: error instanceof Error ? error.message : String(error) });
|
|
1308
|
+
}
|
|
1309
|
+
return true;
|
|
1310
|
+
}
|
|
1311
|
+
if (url.pathname === "/api/render" && req.method === "POST") {
|
|
1312
|
+
if ([...jobs.values()].some((j) => j.state === "running")) {
|
|
1313
|
+
json(res, 409, { error: "a render is already running" });
|
|
1314
|
+
return true;
|
|
1315
|
+
}
|
|
1316
|
+
let body;
|
|
1317
|
+
try {
|
|
1318
|
+
const raw = JSON.parse((await readBody(req)).toString() || "{}");
|
|
1319
|
+
body = validateRenderRequest(raw, await nativeExportCapabilities());
|
|
1320
|
+
} catch (error) {
|
|
1321
|
+
json(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
1322
|
+
return true;
|
|
1323
|
+
}
|
|
1324
|
+
if ([...jobs.values()].some((j) => j.state === "running")) {
|
|
1325
|
+
json(res, 409, { error: "a render is already running" });
|
|
1326
|
+
return true;
|
|
1327
|
+
}
|
|
1328
|
+
const requested = (body.fileName ?? "render.mp4").replace(/[/\\]/g, "_");
|
|
1329
|
+
const name = requested.endsWith(".mp4") ? requested : `${requested}.mp4`;
|
|
1330
|
+
const renders = join5(dir, "renders");
|
|
1331
|
+
mkdirSync4(renders, { recursive: true });
|
|
1332
|
+
const directory = lstatSync2(renders);
|
|
1333
|
+
if (!directory.isDirectory() || directory.isSymbolicLink()) {
|
|
1334
|
+
json(res, 400, { error: "renders must be a project directory, not a symbolic link" });
|
|
1335
|
+
return true;
|
|
1336
|
+
}
|
|
1337
|
+
const stage = mkdtempSync(join5(renders, ".export-"));
|
|
1338
|
+
const stageIdentity = lstatSync2(stage);
|
|
1339
|
+
const out = join5(stage, "output.mp4");
|
|
1340
|
+
const id2 = String(++jobSeq);
|
|
1341
|
+
const job2 = { state: "running", done: 0, total: 0, abort: new AbortController() };
|
|
1342
|
+
jobs.set(id2, job2);
|
|
1343
|
+
void (async () => {
|
|
1344
|
+
try {
|
|
1345
|
+
const report = await render(dir, {
|
|
1346
|
+
out,
|
|
1347
|
+
depth: body.depth ?? 8,
|
|
1348
|
+
allowFontFallback: body.allowFontFallback === true,
|
|
1349
|
+
...body.spec ? { spec: body.spec } : {},
|
|
1350
|
+
...body.sceneId ? { sceneId: body.sceneId } : {},
|
|
1351
|
+
...body.from !== void 0 ? { from: body.from } : {},
|
|
1352
|
+
...body.to !== void 0 ? { to: body.to } : {},
|
|
1353
|
+
signal: job2.abort.signal,
|
|
1354
|
+
onProgress: (done, total) => {
|
|
1355
|
+
job2.done = done;
|
|
1356
|
+
job2.total = total;
|
|
1357
|
+
}
|
|
1358
|
+
});
|
|
1359
|
+
if (job2.abort.signal.aborted) throw Object.assign(new Error("render cancelled"), { name: "RenderCancelledError" });
|
|
1360
|
+
const current = lstatSync2(renders);
|
|
1361
|
+
if (!current.isDirectory() || current.isSymbolicLink() || current.dev !== directory.dev || current.ino !== directory.ino) throw new Error("renders directory changed during export");
|
|
1362
|
+
const output = lstatSync2(out);
|
|
1363
|
+
if (!output.isFile() || output.isSymbolicLink() || output.size === 0) throw new Error("render did not produce a complete file");
|
|
1364
|
+
const occupied = new Set(readdirSync3(renders));
|
|
1365
|
+
for (let n = 1; ; n++) {
|
|
1366
|
+
const candidate = n === 1 ? name : `${name.slice(0, -4)}-${n}.mp4`;
|
|
1367
|
+
if (occupied.has(candidate)) continue;
|
|
1368
|
+
try {
|
|
1369
|
+
linkSync(out, join5(renders, candidate));
|
|
1370
|
+
} catch (error) {
|
|
1371
|
+
const code = error.code;
|
|
1372
|
+
if (code === "EEXIST") continue;
|
|
1373
|
+
if (!["ENOTSUP", "EOPNOTSUPP", "EPERM", "EXDEV", "ENOSYS"].includes(code ?? "")) throw error;
|
|
1374
|
+
const reserved = mkdtempSync(join5(renders, `${name.slice(0, -4)}-`));
|
|
1375
|
+
try {
|
|
1376
|
+
renameSync4(out, join5(reserved, name));
|
|
1377
|
+
} catch (error2) {
|
|
1378
|
+
rmSync3(reserved, { recursive: true, force: true });
|
|
1379
|
+
throw error2;
|
|
1380
|
+
}
|
|
1381
|
+
job2.out = `renders/${reserved.slice(renders.length + 1)}/${name}`;
|
|
1382
|
+
break;
|
|
1383
|
+
}
|
|
1384
|
+
job2.out = `renders/${candidate}`;
|
|
1385
|
+
break;
|
|
1386
|
+
}
|
|
1387
|
+
job2.state = "done";
|
|
1388
|
+
job2.msPerFrame = report.msPerFrame;
|
|
1389
|
+
} catch (error) {
|
|
1390
|
+
job2.state = job2.abort.signal.aborted || error instanceof Error && error.name === "RenderCancelledError" ? "cancelled" : "failed";
|
|
1391
|
+
job2.error = error instanceof Error ? error.message : String(error);
|
|
1392
|
+
} finally {
|
|
1393
|
+
try {
|
|
1394
|
+
const current = lstatSync2(stage);
|
|
1395
|
+
if (current.dev === stageIdentity.dev && current.ino === stageIdentity.ino && !current.isSymbolicLink()) rmSync3(stage, { recursive: true, force: true });
|
|
1396
|
+
} catch (error) {
|
|
1397
|
+
if (error.code !== "ENOENT") console.warn("Could not remove export staging directory:", error instanceof Error ? error.message : String(error));
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
})();
|
|
1401
|
+
json(res, 200, { id: id2 });
|
|
1402
|
+
return true;
|
|
1403
|
+
}
|
|
1404
|
+
const id = url.searchParams.get("id") ?? "";
|
|
1405
|
+
const job = jobs.get(id);
|
|
1406
|
+
if (!job) {
|
|
1407
|
+
json(res, 404, { error: `no render ${id}` });
|
|
1408
|
+
return true;
|
|
1409
|
+
}
|
|
1410
|
+
if (url.pathname === "/api/render/cancel" && job.state === "running") job.abort.abort();
|
|
1411
|
+
const { abort: _abort, ...status } = job;
|
|
1412
|
+
void _abort;
|
|
1413
|
+
json(res, 200, status);
|
|
1414
|
+
return true;
|
|
1415
|
+
},
|
|
1416
|
+
async (req, res, url) => {
|
|
1417
|
+
if (url.pathname !== "/api/select" && !url.pathname.startsWith("/api/select/")) return false;
|
|
1418
|
+
try {
|
|
1419
|
+
if (url.pathname === "/api/select" && req.method === "POST") {
|
|
1420
|
+
requireSelectBackend();
|
|
1421
|
+
const r = selectRequest(dir, (await readBody(req)).toString());
|
|
1422
|
+
json(res, 200, await select(r.file, r.time, r.prompt));
|
|
1423
|
+
return true;
|
|
1424
|
+
}
|
|
1425
|
+
if (url.pathname === "/api/select/tracks" && req.method === "POST") {
|
|
1426
|
+
requireSelectBackend();
|
|
1427
|
+
const r = selectRequest(dir, (await readBody(req)).toString());
|
|
1428
|
+
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`);
|
|
1431
|
+
const id2 = String(tracks.size + 1);
|
|
1432
|
+
const job = { out, state: { status: "running" } };
|
|
1433
|
+
tracks.set(id2, job);
|
|
1434
|
+
track(r.file, {
|
|
1435
|
+
time: r.time,
|
|
1436
|
+
prompt: r.prompt,
|
|
1437
|
+
...r.pick ? { pick: r.pick } : {},
|
|
1438
|
+
...r.start !== void 0 ? { start: r.start } : {},
|
|
1439
|
+
...r.end !== void 0 ? { end: r.end } : {},
|
|
1440
|
+
out,
|
|
1441
|
+
onProgress: (done, total) => {
|
|
1442
|
+
job.state = { status: "running", progress: { done, total } };
|
|
1443
|
+
}
|
|
1444
|
+
}).then(
|
|
1445
|
+
(result) => {
|
|
1446
|
+
job.state = { status: "succeeded", mask: rel(dir, result.path) };
|
|
1447
|
+
},
|
|
1448
|
+
(error) => {
|
|
1449
|
+
job.state = { status: "failed", error: error instanceof Error ? error.message : String(error) };
|
|
1450
|
+
}
|
|
1451
|
+
);
|
|
1452
|
+
json(res, 202, { id: id2 });
|
|
1453
|
+
return true;
|
|
1454
|
+
}
|
|
1455
|
+
const id = /^\/api\/select\/tracks\/([^/]+)$/.exec(url.pathname)?.[1];
|
|
1456
|
+
if (id !== void 0 && req.method === "GET") {
|
|
1457
|
+
const job = tracks.get(decodeURIComponent(id));
|
|
1458
|
+
if (job) json(res, 200, job.state);
|
|
1459
|
+
else json(res, 404, { error: `no track ${decodeURIComponent(id)}` });
|
|
1460
|
+
return true;
|
|
1461
|
+
}
|
|
1462
|
+
json(res, 404, { error: `no route ${req.method ?? "GET"} ${url.pathname}` });
|
|
1463
|
+
} catch (error) {
|
|
1464
|
+
const e = error instanceof SelectError ? error : void 0;
|
|
1465
|
+
json(res, e?.status ?? 500, { error: error instanceof Error ? error.message : String(error), ...e?.code ? { code: e.code } : {} });
|
|
1466
|
+
}
|
|
1467
|
+
return true;
|
|
1468
|
+
},
|
|
1469
|
+
(req, res, url) => {
|
|
1470
|
+
if (!url.pathname.startsWith("/preview/")) return false;
|
|
1471
|
+
const rel2 = decodeURIComponent(url.pathname.slice("/preview/".length));
|
|
1472
|
+
const file = inside(dir, rel2);
|
|
1473
|
+
if (!file) {
|
|
1474
|
+
res.statusCode = 403;
|
|
1475
|
+
res.end();
|
|
1476
|
+
return true;
|
|
1477
|
+
}
|
|
1478
|
+
if (rel2.startsWith(`${PRERENDER_DIR}/`)) {
|
|
1479
|
+
const cached = prerender.lookup(rel2.slice(PRERENDER_DIR.length + 1));
|
|
1480
|
+
if (cached) sendFile(req, res, cached);
|
|
1481
|
+
else json(res, 404, { error: `no pre-rendered preview ${rel2}` });
|
|
1482
|
+
return true;
|
|
1483
|
+
}
|
|
1484
|
+
proxies.consider(rel2, true);
|
|
1485
|
+
const proxy = proxies.lookup(rel2);
|
|
1486
|
+
if (proxy) res.setHeader("x-higgsedit-proxy", "1");
|
|
1487
|
+
sendFile(req, res, proxy ?? file);
|
|
1488
|
+
return true;
|
|
1489
|
+
},
|
|
1490
|
+
async (req, res, url) => {
|
|
1491
|
+
if (url.pathname !== "/api/prerender") return false;
|
|
1492
|
+
if (req.method === "GET") {
|
|
1493
|
+
json(res, 200, { statuses: prerender.statuses() });
|
|
1494
|
+
return true;
|
|
1495
|
+
}
|
|
1496
|
+
try {
|
|
1497
|
+
const body = JSON.parse((await readBody(req)).toString() || "{}");
|
|
1498
|
+
json(res, 200, { statuses: await prerender.request(body.targets ?? [], body.playhead ?? 0, body.client ?? "") });
|
|
1499
|
+
} catch (error) {
|
|
1500
|
+
json(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
1501
|
+
}
|
|
1502
|
+
return true;
|
|
1503
|
+
},
|
|
1504
|
+
(req, res, url) => {
|
|
1505
|
+
if (url.pathname !== "/api/presets") return false;
|
|
1506
|
+
json(res, 200, presetCatalogue(presets));
|
|
1507
|
+
return true;
|
|
1508
|
+
},
|
|
1509
|
+
(req, res, url) => {
|
|
1510
|
+
const id = /^\/api\/presets\/([^/]+)\/preview\.mp4$/.exec(url.pathname)?.[1];
|
|
1511
|
+
if (!id) return false;
|
|
1512
|
+
const preset = readManifest(presets)?.find((p) => p.id === decodeURIComponent(id));
|
|
1513
|
+
if (!preset) {
|
|
1514
|
+
res.statusCode = 404;
|
|
1515
|
+
res.end("not found");
|
|
1516
|
+
return true;
|
|
1517
|
+
}
|
|
1518
|
+
sendFile(req, res, join5(presets, preset.preview));
|
|
1519
|
+
return true;
|
|
1520
|
+
},
|
|
1521
|
+
async (req, res, url) => {
|
|
1522
|
+
if (url.pathname !== "/api/presets/add" || req.method !== "POST") return false;
|
|
1523
|
+
const body = JSON.parse((await readBody(req)).toString());
|
|
1524
|
+
const preset = readManifest(presets)?.find((p) => p.id === body.id);
|
|
1525
|
+
if (!preset) {
|
|
1526
|
+
json(res, 404, { ok: false, error: `no preset ${body.id} installed` });
|
|
1527
|
+
return true;
|
|
1528
|
+
}
|
|
1529
|
+
const tx = history.begin(`Add preset ${preset.name}`);
|
|
1530
|
+
try {
|
|
1531
|
+
const r = addPreset(dir, preset, body, tx.onWrite, presets);
|
|
1532
|
+
if (r.ok) tx.commit();
|
|
1533
|
+
else tx.rollback();
|
|
1534
|
+
if ("error" in r) json(res, 400, r);
|
|
1535
|
+
else json(res, 200, { ...r, undo: history.state() });
|
|
1536
|
+
return true;
|
|
1537
|
+
} catch (error) {
|
|
1538
|
+
tx.rollback();
|
|
1539
|
+
throw error;
|
|
1540
|
+
}
|
|
1541
|
+
},
|
|
1542
|
+
(req, res, url) => {
|
|
1543
|
+
if (url.pathname !== "/api/history") return false;
|
|
1544
|
+
json(res, 200, history.state());
|
|
1545
|
+
void req;
|
|
1546
|
+
return true;
|
|
1547
|
+
},
|
|
1548
|
+
async (req, res, url) => {
|
|
1549
|
+
if (url.pathname !== "/api/media") return false;
|
|
1550
|
+
const media = join5(dir, "media");
|
|
1551
|
+
const files = listMediaFiles(media);
|
|
1552
|
+
json(res, 200, { files });
|
|
1553
|
+
return true;
|
|
1554
|
+
},
|
|
1555
|
+
(req, res, url) => {
|
|
1556
|
+
if (url.pathname !== "/api/analyze") return false;
|
|
1557
|
+
const key = url.searchParams.get("key") ?? "";
|
|
1558
|
+
const names = (url.searchParams.get("names") ?? "").split(",").filter(Boolean);
|
|
1559
|
+
json(res, 200, analyze(dir, key, names));
|
|
1560
|
+
void req;
|
|
1561
|
+
return true;
|
|
1562
|
+
}
|
|
1563
|
+
],
|
|
1564
|
+
port
|
|
1565
|
+
);
|
|
1566
|
+
function stop() {
|
|
1567
|
+
stopping = true;
|
|
1568
|
+
return stopped ??= (async () => {
|
|
1569
|
+
clearTimeout(timer);
|
|
1570
|
+
watcher.close();
|
|
1571
|
+
await mediaUploads.close();
|
|
1572
|
+
proxies.stop();
|
|
1573
|
+
await prerender.stop();
|
|
1574
|
+
for (const c of clients) c.end();
|
|
1575
|
+
await server.close();
|
|
1576
|
+
})();
|
|
1577
|
+
}
|
|
1578
|
+
return { ...server, stop };
|
|
1579
|
+
}
|
|
1580
|
+
async function dev(dir, port, presets) {
|
|
1581
|
+
const server = await startDev(dir, port, { presets, onIdleStop: () => quit() });
|
|
1582
|
+
console.log(`higgsedit dev ${dir}
|
|
1583
|
+
editor ${server.origin}/`);
|
|
1584
|
+
const quit = () => {
|
|
1585
|
+
setTimeout(() => process.exit(0), 3e3).unref();
|
|
1586
|
+
void server.stop().finally(() => process.exit(0));
|
|
1587
|
+
};
|
|
1588
|
+
process.once("SIGTERM", quit);
|
|
1589
|
+
process.once("SIGINT", quit);
|
|
1590
|
+
}
|
|
1591
|
+
export {
|
|
1592
|
+
dev,
|
|
1593
|
+
startDev
|
|
1594
|
+
};
|