@shaztech/video-pipeline 1.0.1
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/bin.js +19 -0
- package/dist/editor/assets/index-DV6clFYC.js +71 -0
- package/dist/editor/assets/index-K7ALkYj6.css +1 -0
- package/dist/editor/index.html +18 -0
- package/package.json +42 -0
- package/src/cli.js +35 -0
- package/src/commands/create.js +43 -0
- package/src/commands/edit.js +57 -0
- package/src/commands/run.js +64 -0
- package/src/commands/validate.js +79 -0
- package/src/executor/index.js +148 -0
- package/src/executor/nodeHandlers/input-file.js +41 -0
- package/src/executor/nodeHandlers/input-folder.js +55 -0
- package/src/executor/nodeHandlers/video-cutter.js +145 -0
- package/src/executor/nodeHandlers/video-stitcher.js +190 -0
- package/src/executor/runner.js +75 -0
- package/src/executor/topoSort.js +61 -0
- package/src/server/index.js +81 -0
- package/src/server/routes.js +123 -0
- package/src/spec/defaultSpec.js +24 -0
- package/src/spec/schema.js +80 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.react-flow{direction:ltr;--xy-edge-stroke-default: #b1b1b7;--xy-edge-stroke-width-default: 1;--xy-edge-stroke-selected-default: #555;--xy-connectionline-stroke-default: #b1b1b7;--xy-connectionline-stroke-width-default: 1;--xy-attribution-background-color-default: rgba(255, 255, 255, .5);--xy-minimap-background-color-default: #fff;--xy-minimap-mask-background-color-default: rgba(240, 240, 240, .6);--xy-minimap-mask-stroke-color-default: transparent;--xy-minimap-mask-stroke-width-default: 1;--xy-minimap-node-background-color-default: #e2e2e2;--xy-minimap-node-stroke-color-default: transparent;--xy-minimap-node-stroke-width-default: 2;--xy-background-color-default: transparent;--xy-background-pattern-dots-color-default: #91919a;--xy-background-pattern-lines-color-default: #eee;--xy-background-pattern-cross-color-default: #e2e2e2;background-color:var(--xy-background-color, var(--xy-background-color-default));--xy-node-color-default: inherit;--xy-node-border-default: 1px solid #1a192b;--xy-node-background-color-default: #fff;--xy-node-group-background-color-default: rgba(240, 240, 240, .25);--xy-node-boxshadow-hover-default: 0 1px 4px 1px rgba(0, 0, 0, .08);--xy-node-boxshadow-selected-default: 0 0 0 .5px #1a192b;--xy-node-border-radius-default: 3px;--xy-handle-background-color-default: #1a192b;--xy-handle-border-color-default: #fff;--xy-selection-background-color-default: rgba(0, 89, 220, .08);--xy-selection-border-default: 1px dotted rgba(0, 89, 220, .8);--xy-controls-button-background-color-default: #fefefe;--xy-controls-button-background-color-hover-default: #f4f4f4;--xy-controls-button-color-default: inherit;--xy-controls-button-color-hover-default: inherit;--xy-controls-button-border-color-default: #eee;--xy-controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, .08);--xy-edge-label-background-color-default: #ffffff;--xy-edge-label-color-default: inherit;--xy-resize-background-color-default: #3367d9}.react-flow.dark{--xy-edge-stroke-default: #3e3e3e;--xy-edge-stroke-width-default: 1;--xy-edge-stroke-selected-default: #727272;--xy-connectionline-stroke-default: #b1b1b7;--xy-connectionline-stroke-width-default: 1;--xy-attribution-background-color-default: rgba(150, 150, 150, .25);--xy-minimap-background-color-default: #141414;--xy-minimap-mask-background-color-default: rgba(60, 60, 60, .6);--xy-minimap-mask-stroke-color-default: transparent;--xy-minimap-mask-stroke-width-default: 1;--xy-minimap-node-background-color-default: #2b2b2b;--xy-minimap-node-stroke-color-default: transparent;--xy-minimap-node-stroke-width-default: 2;--xy-background-color-default: #141414;--xy-background-pattern-dots-color-default: #777;--xy-background-pattern-lines-color-default: #777;--xy-background-pattern-cross-color-default: #777;--xy-node-color-default: #f8f8f8;--xy-node-border-default: 1px solid #3c3c3c;--xy-node-background-color-default: #1e1e1e;--xy-node-group-background-color-default: rgba(240, 240, 240, .25);--xy-node-boxshadow-hover-default: 0 1px 4px 1px rgba(255, 255, 255, .08);--xy-node-boxshadow-selected-default: 0 0 0 .5px #999;--xy-handle-background-color-default: #bebebe;--xy-handle-border-color-default: #1e1e1e;--xy-selection-background-color-default: rgba(200, 200, 220, .08);--xy-selection-border-default: 1px dotted rgba(200, 200, 220, .8);--xy-controls-button-background-color-default: #2b2b2b;--xy-controls-button-background-color-hover-default: #3e3e3e;--xy-controls-button-color-default: #f8f8f8;--xy-controls-button-color-hover-default: #fff;--xy-controls-button-border-color-default: #5b5b5b;--xy-controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, .08);--xy-edge-label-background-color-default: #141414;--xy-edge-label-color-default: #f8f8f8}.react-flow__background{background-color:var(--xy-background-color-props, var(--xy-background-color, var(--xy-background-color-default)));pointer-events:none;z-index:-1}.react-flow__container{position:absolute;width:100%;height:100%;top:0;left:0}.react-flow__pane{z-index:1}.react-flow__pane.draggable{cursor:grab}.react-flow__pane.dragging{cursor:grabbing}.react-flow__pane.selection{cursor:pointer}.react-flow__viewport{transform-origin:0 0;z-index:2;pointer-events:none}.react-flow__renderer{z-index:4}.react-flow__selection{z-index:6}.react-flow__nodesselection-rect:focus,.react-flow__nodesselection-rect:focus-visible{outline:none}.react-flow__edge-path{stroke:var(--xy-edge-stroke, var(--xy-edge-stroke-default));stroke-width:var(--xy-edge-stroke-width, var(--xy-edge-stroke-width-default));fill:none}.react-flow__connection-path{stroke:var(--xy-connectionline-stroke, var(--xy-connectionline-stroke-default));stroke-width:var(--xy-connectionline-stroke-width, var(--xy-connectionline-stroke-width-default));fill:none}.react-flow .react-flow__edges{position:absolute}.react-flow .react-flow__edges svg{overflow:visible;position:absolute;pointer-events:none}.react-flow__edge{pointer-events:visibleStroke}.react-flow__edge.selectable{cursor:pointer}.react-flow__edge.animated path{stroke-dasharray:5;animation:dashdraw .5s linear infinite}.react-flow__edge.animated path.react-flow__edge-interaction{stroke-dasharray:none;animation:none}.react-flow__edge.inactive{pointer-events:none}.react-flow__edge.selected,.react-flow__edge:focus,.react-flow__edge:focus-visible{outline:none}.react-flow__edge.selected .react-flow__edge-path,.react-flow__edge.selectable:focus .react-flow__edge-path,.react-flow__edge.selectable:focus-visible .react-flow__edge-path{stroke:var(--xy-edge-stroke-selected, var(--xy-edge-stroke-selected-default))}.react-flow__edge-textwrapper{pointer-events:all}.react-flow__edge .react-flow__edge-text{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.react-flow__arrowhead polyline{stroke:var(--xy-edge-stroke, var(--xy-edge-stroke-default))}.react-flow__arrowhead polyline.arrowclosed{fill:var(--xy-edge-stroke, var(--xy-edge-stroke-default))}.react-flow__connection{pointer-events:none}.react-flow__connection .animated{stroke-dasharray:5;animation:dashdraw .5s linear infinite}svg.react-flow__connectionline{z-index:1001;overflow:visible;position:absolute}.react-flow__nodes{pointer-events:none;transform-origin:0 0}.react-flow__node{position:absolute;-webkit-user-select:none;-moz-user-select:none;user-select:none;pointer-events:all;transform-origin:0 0;box-sizing:border-box;cursor:default}.react-flow__node.selectable{cursor:pointer}.react-flow__node.draggable{cursor:grab;pointer-events:all}.react-flow__node.draggable.dragging{cursor:grabbing}.react-flow__nodesselection{z-index:3;transform-origin:left top;pointer-events:none}.react-flow__nodesselection-rect{position:absolute;pointer-events:all;cursor:grab}.react-flow__handle{position:absolute;pointer-events:none;min-width:5px;min-height:5px;width:6px;height:6px;background-color:var(--xy-handle-background-color, var(--xy-handle-background-color-default));border:1px solid var(--xy-handle-border-color, var(--xy-handle-border-color-default));border-radius:100%}.react-flow__handle.connectingfrom{pointer-events:all}.react-flow__handle.connectionindicator{pointer-events:all;cursor:crosshair}.react-flow__handle-bottom{top:auto;left:50%;bottom:0;transform:translate(-50%,50%)}.react-flow__handle-top{top:0;left:50%;transform:translate(-50%,-50%)}.react-flow__handle-left{top:50%;left:0;transform:translate(-50%,-50%)}.react-flow__handle-right{top:50%;right:0;transform:translate(50%,-50%)}.react-flow__edgeupdater{cursor:move;pointer-events:all}.react-flow__pane.selection .react-flow__panel{pointer-events:none}.react-flow__panel{position:absolute;z-index:5;margin:15px}.react-flow__panel.top{top:0}.react-flow__panel.bottom{bottom:0}.react-flow__panel.top.center,.react-flow__panel.bottom.center{left:50%;transform:translate(-15px) translate(-50%)}.react-flow__panel.left{left:0}.react-flow__panel.right{right:0}.react-flow__panel.left.center,.react-flow__panel.right.center{top:50%;transform:translateY(-15px) translateY(-50%)}.react-flow__attribution{font-size:10px;background:var(--xy-attribution-background-color, var(--xy-attribution-background-color-default));padding:2px 3px;margin:0}.react-flow__attribution a{text-decoration:none;color:#999}@keyframes dashdraw{0%{stroke-dashoffset:10}}.react-flow__edgelabel-renderer{position:absolute;width:100%;height:100%;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;left:0;top:0}.react-flow__viewport-portal{position:absolute;width:100%;height:100%;left:0;top:0;-webkit-user-select:none;-moz-user-select:none;user-select:none}.react-flow__minimap{background:var( --xy-minimap-background-color-props, var(--xy-minimap-background-color, var(--xy-minimap-background-color-default)) )}.react-flow__minimap-svg{display:block}.react-flow__minimap-mask{fill:var( --xy-minimap-mask-background-color-props, var(--xy-minimap-mask-background-color, var(--xy-minimap-mask-background-color-default)) );stroke:var( --xy-minimap-mask-stroke-color-props, var(--xy-minimap-mask-stroke-color, var(--xy-minimap-mask-stroke-color-default)) );stroke-width:var( --xy-minimap-mask-stroke-width-props, var(--xy-minimap-mask-stroke-width, var(--xy-minimap-mask-stroke-width-default)) )}.react-flow__minimap-node{fill:var( --xy-minimap-node-background-color-props, var(--xy-minimap-node-background-color, var(--xy-minimap-node-background-color-default)) );stroke:var( --xy-minimap-node-stroke-color-props, var(--xy-minimap-node-stroke-color, var(--xy-minimap-node-stroke-color-default)) );stroke-width:var( --xy-minimap-node-stroke-width-props, var(--xy-minimap-node-stroke-width, var(--xy-minimap-node-stroke-width-default)) )}.react-flow__background-pattern.dots{fill:var( --xy-background-pattern-color-props, var(--xy-background-pattern-color, var(--xy-background-pattern-dots-color-default)) )}.react-flow__background-pattern.lines{stroke:var( --xy-background-pattern-color-props, var(--xy-background-pattern-color, var(--xy-background-pattern-lines-color-default)) )}.react-flow__background-pattern.cross{stroke:var( --xy-background-pattern-color-props, var(--xy-background-pattern-color, var(--xy-background-pattern-cross-color-default)) )}.react-flow__controls{display:flex;flex-direction:column;box-shadow:var(--xy-controls-box-shadow, var(--xy-controls-box-shadow-default))}.react-flow__controls.horizontal{flex-direction:row}.react-flow__controls-button{display:flex;justify-content:center;align-items:center;height:26px;width:26px;padding:4px;border:none;background:var(--xy-controls-button-background-color, var(--xy-controls-button-background-color-default));border-bottom:1px solid var( --xy-controls-button-border-color-props, var(--xy-controls-button-border-color, var(--xy-controls-button-border-color-default)) );color:var( --xy-controls-button-color-props, var(--xy-controls-button-color, var(--xy-controls-button-color-default)) );cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none}.react-flow__controls-button svg{width:100%;max-width:12px;max-height:12px;fill:currentColor}.react-flow__edge.updating .react-flow__edge-path{stroke:#777}.react-flow__edge-text{font-size:10px}.react-flow__node.selectable:focus,.react-flow__node.selectable:focus-visible{outline:none}.react-flow__node-input,.react-flow__node-default,.react-flow__node-output,.react-flow__node-group{padding:10px;border-radius:var(--xy-node-border-radius, var(--xy-node-border-radius-default));width:150px;font-size:12px;color:var(--xy-node-color, var(--xy-node-color-default));text-align:center;border:var(--xy-node-border, var(--xy-node-border-default));background-color:var(--xy-node-background-color, var(--xy-node-background-color-default))}.react-flow__node-input.selectable:hover,.react-flow__node-default.selectable:hover,.react-flow__node-output.selectable:hover,.react-flow__node-group.selectable:hover{box-shadow:var(--xy-node-boxshadow-hover, var(--xy-node-boxshadow-hover-default))}.react-flow__node-input.selectable.selected,.react-flow__node-input.selectable:focus,.react-flow__node-input.selectable:focus-visible,.react-flow__node-default.selectable.selected,.react-flow__node-default.selectable:focus,.react-flow__node-default.selectable:focus-visible,.react-flow__node-output.selectable.selected,.react-flow__node-output.selectable:focus,.react-flow__node-output.selectable:focus-visible,.react-flow__node-group.selectable.selected,.react-flow__node-group.selectable:focus,.react-flow__node-group.selectable:focus-visible{box-shadow:var(--xy-node-boxshadow-selected, var(--xy-node-boxshadow-selected-default))}.react-flow__node-group{background-color:var(--xy-node-group-background-color, var(--xy-node-group-background-color-default))}.react-flow__nodesselection-rect,.react-flow__selection{background:var(--xy-selection-background-color, var(--xy-selection-background-color-default));border:var(--xy-selection-border, var(--xy-selection-border-default))}.react-flow__nodesselection-rect:focus,.react-flow__nodesselection-rect:focus-visible,.react-flow__selection:focus,.react-flow__selection:focus-visible{outline:none}.react-flow__controls-button:hover{background:var( --xy-controls-button-background-color-hover-props, var(--xy-controls-button-background-color-hover, var(--xy-controls-button-background-color-hover-default)) );color:var( --xy-controls-button-color-hover-props, var(--xy-controls-button-color-hover, var(--xy-controls-button-color-hover-default)) )}.react-flow__controls-button:disabled{pointer-events:none}.react-flow__controls-button:disabled svg{fill-opacity:.4}.react-flow__controls-button:last-child{border-bottom:none}.react-flow__controls.horizontal .react-flow__controls-button{border-bottom:none;border-right:1px solid var( --xy-controls-button-border-color-props, var(--xy-controls-button-border-color, var(--xy-controls-button-border-color-default)) )}.react-flow__controls.horizontal .react-flow__controls-button:last-child{border-right:none}.react-flow__resize-control{position:absolute}.react-flow__resize-control.left,.react-flow__resize-control.right{cursor:ew-resize}.react-flow__resize-control.top,.react-flow__resize-control.bottom{cursor:ns-resize}.react-flow__resize-control.top.left,.react-flow__resize-control.bottom.right{cursor:nwse-resize}.react-flow__resize-control.bottom.left,.react-flow__resize-control.top.right{cursor:nesw-resize}.react-flow__resize-control.handle{width:5px;height:5px;border:1px solid #fff;border-radius:1px;background-color:var(--xy-resize-background-color, var(--xy-resize-background-color-default));translate:-50% -50%}.react-flow__resize-control.handle.left{left:0;top:50%}.react-flow__resize-control.handle.right{left:100%;top:50%}.react-flow__resize-control.handle.top{left:50%;top:0}.react-flow__resize-control.handle.bottom{left:50%;top:100%}.react-flow__resize-control.handle.top.left,.react-flow__resize-control.handle.bottom.left{left:0}.react-flow__resize-control.handle.top.right,.react-flow__resize-control.handle.bottom.right{left:100%}.react-flow__resize-control.line{border-color:var(--xy-resize-background-color, var(--xy-resize-background-color-default));border-width:0;border-style:solid}.react-flow__resize-control.line.left,.react-flow__resize-control.line.right{width:1px;transform:translate(-50%);top:0;height:100%}.react-flow__resize-control.line.left{left:0;border-left-width:1px}.react-flow__resize-control.line.right{left:100%;border-right-width:1px}.react-flow__resize-control.line.top,.react-flow__resize-control.line.bottom{height:1px;transform:translateY(-50%);left:0;width:100%}.react-flow__resize-control.line.top{top:0;border-top-width:1px}.react-flow__resize-control.line.bottom{border-bottom-width:1px;top:100%}.react-flow__edge-textbg{fill:var(--xy-edge-label-background-color, var(--xy-edge-label-background-color-default))}.react-flow__edge-text{fill:var(--xy-edge-label-color, var(--xy-edge-label-color-default))}._toolbar_1uqg7_1{position:absolute;top:0;left:0;right:0;z-index:10;display:flex;align-items:center;justify-content:space-between;padding:8px 14px;background:#0d1117eb;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);border-bottom:1px solid #21262d}._left_1uqg7_16{display:flex;align-items:center;gap:10px}._right_1uqg7_22{display:flex;align-items:center;gap:8px}._pipelineName_1uqg7_28{font-size:13px;font-weight:700;color:#e0e0e0;letter-spacing:.02em;cursor:text}._pipelineName_1uqg7_28:hover{color:#fff}._pipelineNameInput_1uqg7_40{font-size:13px;font-weight:700;color:#e0e0e0;letter-spacing:.02em;background:transparent;border:none;border-bottom:1px solid #388bfd;outline:none;padding:0;min-width:80px;width:auto}._divider_1uqg7_54{width:1px;height:20px;background:#21262d}._addBtn_1uqg7_60{border:none;border-radius:6px;font-size:12px;font-weight:600;padding:6px 12px;cursor:pointer;transition:opacity .15s,transform .1s}._addBtn_1uqg7_60:hover{opacity:.85;transform:translateY(-1px)}._addBtn_1uqg7_60:active{transform:translateY(0)}._cutter_1uqg7_79{background:#e9456026;color:#e94560;border:1px solid rgba(233,69,96,.4)}._stitcher_1uqg7_85{background:#53348333;color:#a371f7;border:1px solid rgba(163,113,247,.4)}._outputFolder_1uqg7_91{background:#2ea04326;color:#3fb950;border:1px solid rgba(46,160,67,.4)}._inputFile_1uqg7_97{background:#388bfd26;color:#388bfd;border:1px solid rgba(56,139,253,.4)}._inputFolder_1uqg7_103{background:#d2992226;color:#d29922;border:1px solid rgba(210,153,34,.4)}._saveBtn_1uqg7_109{background:#21262d;border:1px solid #30363d;border-radius:6px;color:#c9d1d9;font-size:12px;font-weight:600;padding:6px 14px;cursor:pointer;position:relative;transition:background .15s,border-color .15s,color .15s;display:flex;align-items:center;gap:6px}._saveBtn_1uqg7_109:hover:not(:disabled){background:#30363d;border-color:#8b949e}._saveBtn_1uqg7_109:disabled{opacity:.6;cursor:default}._dirty_1uqg7_135{border-color:#e94560;color:#e94560}._savedGreen_1uqg7_140{color:#3fb950;border-color:#3fb95066}._errorRed_1uqg7_145{color:#f85149;border-color:#f8514966}._serverStatus_1uqg7_150{display:flex;align-items:center;gap:5px;font-size:11px;font-weight:600;color:#484f58;transition:color .3s}._serverStatus_1uqg7_150._online_1uqg7_160{color:#3fb950}._serverStatus_1uqg7_150._offline_1uqg7_161{color:#f85149}._serverDot_1uqg7_163{width:7px;height:7px;border-radius:50%;flex-shrink:0;background:currentColor;transition:box-shadow .3s}._serverStatus_1uqg7_150._online_1uqg7_160 ._serverDot_1uqg7_163{box-shadow:0 0 5px #3fb950b3}._serverStatus_1uqg7_150._offline_1uqg7_161 ._serverDot_1uqg7_163{box-shadow:0 0 5px #f85149b3}._serverLabel_1uqg7_175{white-space:nowrap}._saveIcon_1uqg7_179{width:13px;height:13px;flex-shrink:0}._node_9qeze_1{background:#16213e;border:1.5px solid #0f3460;border-radius:10px;min-width:260px;max-width:320px;box-shadow:0 4px 20px #00000080;font-size:12px;color:#c9d1d9;transition:border-color .15s;position:relative}._deleteBtn_9qeze_14{position:absolute;top:-8px;right:-8px;width:18px;height:18px;border-radius:50%;background:#21262d;border:1.5px solid #30363d;color:#6e7681;font-size:11px;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0;opacity:0;transition:opacity .15s,background .15s,color .15s;z-index:10}._node_9qeze_1:hover ._deleteBtn_9qeze_14{opacity:1}._deleteBtn_9qeze_14:hover{background:#e94560;border-color:#e94560;color:#fff}._node_9qeze_1._selected_9qeze_46{border-color:#e94560;box-shadow:0 0 0 2px #e9456040,0 4px 20px #00000080}._cutter_9qeze_51{border-top:3px solid #e94560}._stitcher_9qeze_52{border-top:3px solid #533483}._outputFolder_9qeze_53{border-top:3px solid #2ea043}._inputFile_9qeze_54{border-top:3px solid #388bfd}._inputFolder_9qeze_55{border-top:3px solid #d29922}._header_9qeze_57{display:flex;align-items:center;gap:6px;padding:8px 10px 6px;border-bottom:1px solid #0f3460}._badge_9qeze_65{font-size:10px;font-weight:700;letter-spacing:.03em;color:#8b949e;text-transform:uppercase;white-space:nowrap}._labelInput_9qeze_74{flex:1;background:transparent;border:none;outline:none;color:#e0e0e0;font-size:13px;font-weight:600;min-width:0}._body_9qeze_85{padding:10px;display:flex;flex-direction:column;gap:5px}._fieldLabel_9qeze_92{font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:#8b949e;margin-top:4px}._input_9qeze_54{width:100%;background:#0d1117;border:1px solid #21262d;border-radius:5px;color:#c9d1d9;font-size:12px;padding:5px 8px;outline:none;transition:border-color .15s}._input_9qeze_54:focus{border-color:#388bfd}._radioGroup_9qeze_116{display:flex;flex-direction:column;gap:3px}._radio_9qeze_116{display:flex;align-items:center;gap:5px;cursor:pointer;font-size:11px;color:#c9d1d9}._checkboxRow_9qeze_131{display:flex;gap:12px;margin-top:2px}._checkbox_9qeze_131{display:flex;align-items:center;gap:4px;cursor:pointer;font-size:11px;color:#c9d1d9}._sectionHeader_9qeze_146{display:flex;align-items:center;justify-content:space-between;margin-top:4px}._addBtn_9qeze_153{background:#21262d;border:1px solid #30363d;border-radius:4px;color:#8b949e;font-size:10px;padding:2px 7px;cursor:pointer;transition:background .1s}._addBtn_9qeze_153:hover{background:#30363d;color:#c9d1d9}._inputRow_9qeze_169{display:flex;flex-direction:column;gap:3px}._inputRowMain_9qeze_175{display:flex;gap:4px;align-items:center}._removeBtn_9qeze_181{background:none;border:none;color:#6e7681;cursor:pointer;font-size:12px;padding:2px 4px;border-radius:3px;flex-shrink:0;transition:color .1s}._removeBtn_9qeze_181:hover{color:#e94560}._hint_9qeze_197{font-size:10px;color:#6e7681;font-style:italic;margin-bottom:2px}._dragHandle_9qeze_204{cursor:grab;color:#6e7681;font-size:14px;padding:0 4px;flex-shrink:0;-webkit-user-select:none;user-select:none;line-height:1;touch-action:none}._dragHandle_9qeze_204:hover{color:#c9d1d9}._dragHandle_9qeze_204:active{cursor:grabbing}._inputRow_9qeze_169._dragging_9qeze_223{opacity:.35}._inputRow_9qeze_169._dropTarget_9qeze_227{border-top:2px solid #388bfd;margin-top:-2px}._inputRow_9qeze_169._dropAfter_9qeze_232{border-bottom:2px solid #388bfd;margin-bottom:-2px}._edgeItem_9qeze_237{flex:1;display:flex;align-items:center;gap:6px;background:#0d1117;border:1px solid #21262d;border-radius:5px;padding:5px 8px;color:#8b949e;font-size:12px;min-width:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}._stepperRow_9qeze_254{display:flex;gap:4px;align-items:stretch}._stepperRow_9qeze_254 ._input_9qeze_54{flex:1}._stepperBtns_9qeze_264{display:flex;flex-direction:column;gap:1px;flex-shrink:0}._stepBtn_9qeze_271{background:#21262d;border:1px solid #30363d;border-radius:3px;color:#8b949e;font-size:8px;padding:0 5px;cursor:pointer;line-height:1;flex:1;transition:background .1s,color .1s}._stepBtn_9qeze_271:hover{background:#30363d;color:#c9d1d9}._pencilBtn_9qeze_289{background:none;border:1px solid transparent;border-radius:3px;color:#6e7681;cursor:pointer;font-size:13px;padding:1px 4px;flex-shrink:0;transition:color .1s,border-color .1s;line-height:1.2}._pencilBtn_9qeze_289:hover{color:#c9d1d9;border-color:#30363d}._pencilBtn_9qeze_289._pencilActive_9qeze_307{color:#388bfd;border-color:#388bfd}._durationOverrideRow_9qeze_312{display:flex;align-items:center;gap:4px;padding-left:22px}._durationOverrideLabel_9qeze_319{font-size:10px;color:#6e7681;white-space:nowrap;flex-shrink:0}._resetBtn_9qeze_326{background:none;border:1px solid #30363d;border-radius:3px;color:#6e7681;cursor:pointer;font-size:12px;padding:1px 5px;flex-shrink:0;transition:color .1s;line-height:1.4}._resetBtn_9qeze_326:hover{color:#c9d1d9}._edgeBadge_9qeze_343{font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:#a371f7;background:#1a1030;border:1px solid #533483;border-radius:3px;padding:1px 4px;flex-shrink:0}._handle_9qeze_356{width:10px;height:10px;background:#533483;border:2px solid #a371f7;border-radius:50%}._container_1ymg3_1{display:flex;flex-direction:column;gap:2px;width:100%}._wrapper_1ymg3_8{display:flex;align-items:center;background:#0d1117;border:1px solid #21262d;border-radius:5px;transition:border-color .15s,background .15s;overflow:hidden;width:100%}._wrapper_1ymg3_8:focus-within{border-color:#388bfd}._wrapper_1ymg3_8._dragOver_1ymg3_23{border-color:#388bfd;background:#0d1a2d}._textInput_1ymg3_28{flex:1;background:transparent;border:none;color:#c9d1d9;font-size:12px;padding:5px 8px;outline:none;min-width:0}._textInput_1ymg3_28::placeholder{color:#484f58}._browseBtn_1ymg3_43{background:none;border:none;border-left:1px solid #21262d;color:#6e7681;cursor:pointer;font-size:12px;padding:4px 7px;line-height:1;flex-shrink:0;transition:color .1s,background .1s}._browseBtn_1ymg3_43:hover{color:#c9d1d9;background:#21262d}._wrapper_1ymg3_8._error_1ymg3_61{border-color:#e94560}._errorMsg_1ymg3_65{font-size:10px;color:#e94560;padding:0 2px}._hidden_1ymg3_71{display:none}.app{height:100%;display:flex;flex-direction:column}.canvas-wrapper{flex:1;margin-top:49px}.react-flow__edge-path{stroke:#8b949e;stroke-width:2}.react-flow__edge.selected .react-flow__edge-path,.react-flow__edge:hover .react-flow__edge-path{stroke:#a371f7}.react-flow__connection-path{stroke:#a371f7;stroke-width:2}.react-flow__controls{background:#0d1117;border:1px solid #21262d;border-radius:6px;overflow:hidden}.react-flow__controls-button{background:#0d1117;border-bottom:1px solid #21262d;fill:#8b949e}.react-flow__controls-button:hover{background:#21262d;fill:#c9d1d9}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Video Pipeline Editor</title>
|
|
7
|
+
<style>
|
|
8
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9
|
+
html, body, #root { height: 100%; width: 100%; overflow: hidden; }
|
|
10
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #1a1a2e; color: #e0e0e0; }
|
|
11
|
+
</style>
|
|
12
|
+
<script type="module" crossorigin src="./assets/index-DV6clFYC.js"></script>
|
|
13
|
+
<link rel="stylesheet" crossorigin href="./assets/index-K7ALkYj6.css">
|
|
14
|
+
</head>
|
|
15
|
+
<body>
|
|
16
|
+
<div id="root"></div>
|
|
17
|
+
</body>
|
|
18
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@shaztech/video-pipeline",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Visual node-based video processing pipeline CLI",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"video-pipeline": "./bin.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin.js",
|
|
12
|
+
"src/",
|
|
13
|
+
"dist/"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=20.0.0"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@shaztech/video-cutter": "^1.0.0",
|
|
20
|
+
"@shaztech/video-stitcher": "^1.0.0",
|
|
21
|
+
"ajv": "^8.18.0",
|
|
22
|
+
"chalk": "^5.3.0",
|
|
23
|
+
"commander": "^14.0.0",
|
|
24
|
+
"execa": "^9.5.2",
|
|
25
|
+
"express": "^5.0.1",
|
|
26
|
+
"glob": "^11.0.0",
|
|
27
|
+
"open": "^10.1.0",
|
|
28
|
+
"ora": "^8.0.1",
|
|
29
|
+
"p-queue": "^8.1.0",
|
|
30
|
+
"ws": "^8.17.0"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"test": "vitest run --coverage"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@vitest/coverage-v8": "^4.1.1",
|
|
40
|
+
"vitest": "^4.1.1"
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Shazron Abdullah
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { Command } from 'commander'
|
|
18
|
+
import { createCommand } from './commands/create.js'
|
|
19
|
+
import { editCommand } from './commands/edit.js'
|
|
20
|
+
import { runCommand } from './commands/run.js'
|
|
21
|
+
import { validateCommand } from './commands/validate.js'
|
|
22
|
+
|
|
23
|
+
const program = new Command()
|
|
24
|
+
|
|
25
|
+
program
|
|
26
|
+
.name('video-pipeline')
|
|
27
|
+
.description('Visual node-based video processing pipeline')
|
|
28
|
+
.version('1.0.0')
|
|
29
|
+
|
|
30
|
+
program.addCommand(createCommand())
|
|
31
|
+
program.addCommand(editCommand())
|
|
32
|
+
program.addCommand(runCommand())
|
|
33
|
+
program.addCommand(validateCommand())
|
|
34
|
+
|
|
35
|
+
program.parse()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Shazron Abdullah
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { Command } from 'commander'
|
|
18
|
+
import { writeFileSync, existsSync } from 'fs'
|
|
19
|
+
import path from 'path'
|
|
20
|
+
import chalk from 'chalk'
|
|
21
|
+
import { createDefaultSpec } from '../spec/defaultSpec.js'
|
|
22
|
+
|
|
23
|
+
export function createCommand() {
|
|
24
|
+
return new Command('create')
|
|
25
|
+
.description('Create a new pipeline spec file')
|
|
26
|
+
.argument('[name]', 'Pipeline name (also used as filename)', 'pipeline')
|
|
27
|
+
.option('-f, --force', 'Overwrite if file already exists')
|
|
28
|
+
.action((name, opts) => {
|
|
29
|
+
const filename = name.endsWith('.json') ? name : `${name}.json`
|
|
30
|
+
const filepath = path.resolve(process.cwd(), filename)
|
|
31
|
+
|
|
32
|
+
if (existsSync(filepath) && !opts.force) {
|
|
33
|
+
console.error(chalk.red(`File already exists: ${filename}`))
|
|
34
|
+
console.error(chalk.dim('Use --force to overwrite'))
|
|
35
|
+
process.exit(1)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const spec = createDefaultSpec(name.replace(/\.json$/, ''))
|
|
39
|
+
writeFileSync(filepath, JSON.stringify(spec, null, 2) + '\n', 'utf8')
|
|
40
|
+
console.log(chalk.green(`Created: ${filename}`))
|
|
41
|
+
console.log(chalk.dim(`Edit it visually: video-pipeline edit ${filename}`))
|
|
42
|
+
})
|
|
43
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Shazron Abdullah
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { Command } from 'commander'
|
|
18
|
+
import { existsSync, writeFileSync } from 'fs'
|
|
19
|
+
import path from 'path'
|
|
20
|
+
import chalk from 'chalk'
|
|
21
|
+
import { createDefaultSpec } from '../spec/defaultSpec.js'
|
|
22
|
+
import { startEditorServer } from '../server/index.js'
|
|
23
|
+
|
|
24
|
+
export function editCommand() {
|
|
25
|
+
return new Command('edit')
|
|
26
|
+
.description('Open the visual pipeline editor in a browser')
|
|
27
|
+
.argument('[spec]', 'Path to pipeline spec JSON file', 'pipeline.json')
|
|
28
|
+
.option('-p, --port <port>', 'Server port (default: auto-detect)', parseInt)
|
|
29
|
+
.action(async (specArg, opts) => {
|
|
30
|
+
const specPath = path.resolve(process.cwd(), specArg)
|
|
31
|
+
|
|
32
|
+
if (!existsSync(specPath)) {
|
|
33
|
+
const name = path.basename(specArg, '.json')
|
|
34
|
+
const spec = createDefaultSpec(name)
|
|
35
|
+
writeFileSync(specPath, JSON.stringify(spec, null, 2) + '\n', 'utf8')
|
|
36
|
+
console.log(chalk.dim(`Created new spec: ${specArg}`))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const { url } = await startEditorServer(specPath, { port: opts.port })
|
|
40
|
+
|
|
41
|
+
console.log(chalk.green(`\n Editor ready: ${url}\n`))
|
|
42
|
+
console.log(chalk.dim(' Press Ctrl+C to stop\n'))
|
|
43
|
+
|
|
44
|
+
const { default: open } = await import('open')
|
|
45
|
+
await open(url)
|
|
46
|
+
|
|
47
|
+
// Keep process alive
|
|
48
|
+
await new Promise((resolve) => {
|
|
49
|
+
process.once('SIGINT', () => {
|
|
50
|
+
console.log(chalk.dim('\n Editor closed.'))
|
|
51
|
+
resolve()
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
process.exit(0)
|
|
56
|
+
})
|
|
57
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Shazron Abdullah
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { Command } from 'commander'
|
|
18
|
+
import { readFileSync, existsSync } from 'fs'
|
|
19
|
+
import chalk from 'chalk'
|
|
20
|
+
import { validateSpec } from '../spec/schema.js'
|
|
21
|
+
import { executePipeline } from '../executor/index.js'
|
|
22
|
+
|
|
23
|
+
export function runCommand() {
|
|
24
|
+
return new Command('run')
|
|
25
|
+
.description('Execute a pipeline from a spec file')
|
|
26
|
+
.argument('<spec>', 'Path to pipeline spec JSON file')
|
|
27
|
+
.option('--keep-temp', 'Keep intermediate temp files after execution')
|
|
28
|
+
.option('--dry-run', 'Print execution plan without running')
|
|
29
|
+
.option('--overwrite', 'Overwrite output files if they already exist')
|
|
30
|
+
.action(async (specArg, opts) => {
|
|
31
|
+
if (!existsSync(specArg)) {
|
|
32
|
+
console.error(chalk.red(`File not found: ${specArg}`))
|
|
33
|
+
process.exit(1)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let spec
|
|
37
|
+
try {
|
|
38
|
+
spec = JSON.parse(readFileSync(specArg, 'utf8'))
|
|
39
|
+
} catch (err) {
|
|
40
|
+
console.error(chalk.red(`Invalid JSON: ${err.message}`))
|
|
41
|
+
process.exit(1)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const validation = validateSpec(spec)
|
|
45
|
+
if (!validation.valid) {
|
|
46
|
+
console.error(chalk.red('Invalid pipeline spec:'))
|
|
47
|
+
for (const err of validation.errors) {
|
|
48
|
+
console.error(chalk.red(` • ${err}`))
|
|
49
|
+
}
|
|
50
|
+
process.exit(1)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
await executePipeline(spec, {
|
|
55
|
+
keepTemp: opts.keepTemp ?? false,
|
|
56
|
+
dryRun: opts.dryRun ?? false,
|
|
57
|
+
overwrite: opts.overwrite ?? false
|
|
58
|
+
})
|
|
59
|
+
} catch (err) {
|
|
60
|
+
console.error(chalk.red(`Pipeline failed: ${err.message}`))
|
|
61
|
+
process.exit(1)
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Shazron Abdullah
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { Command } from 'commander'
|
|
18
|
+
import { readFileSync, existsSync } from 'fs'
|
|
19
|
+
import { fileURLToPath } from 'url'
|
|
20
|
+
import path from 'path'
|
|
21
|
+
import chalk from 'chalk'
|
|
22
|
+
import Ajv from 'ajv'
|
|
23
|
+
import { validateSpec } from '../spec/schema.js'
|
|
24
|
+
|
|
25
|
+
// Resolve the schema relative to the repo root (works both installed and local)
|
|
26
|
+
const here = path.dirname(fileURLToPath(import.meta.url))
|
|
27
|
+
const schemaPath = path.resolve(here, '../../../../pipeline.schema.json')
|
|
28
|
+
const schema = JSON.parse(readFileSync(schemaPath, 'utf8'))
|
|
29
|
+
|
|
30
|
+
const ajv = new Ajv({ allErrors: true, strict: false })
|
|
31
|
+
const ajvValidate = ajv.compile(schema)
|
|
32
|
+
|
|
33
|
+
export function validateCommand() {
|
|
34
|
+
return new Command('validate')
|
|
35
|
+
.description('Validate a pipeline spec file against the JSON schema and semantic rules')
|
|
36
|
+
.argument('<spec>', 'Path to pipeline spec JSON file')
|
|
37
|
+
.action((specArg) => {
|
|
38
|
+
if (!existsSync(specArg)) {
|
|
39
|
+
console.error(chalk.red(`File not found: ${specArg}`))
|
|
40
|
+
process.exit(1)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let spec
|
|
44
|
+
try {
|
|
45
|
+
spec = JSON.parse(readFileSync(specArg, 'utf8'))
|
|
46
|
+
} catch (err) {
|
|
47
|
+
console.error(chalk.red(`Invalid JSON: ${err.message}`))
|
|
48
|
+
process.exit(1)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const errors = []
|
|
52
|
+
|
|
53
|
+
// 1. JSON Schema validation
|
|
54
|
+
const schemaValid = ajvValidate(spec)
|
|
55
|
+
if (!schemaValid) {
|
|
56
|
+
for (const err of ajvValidate.errors) {
|
|
57
|
+
const loc = err.instancePath || '(root)'
|
|
58
|
+
errors.push(`Schema: ${loc} ${err.message}`)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 2. Semantic validation (duplicate ids, edge references, etc.)
|
|
63
|
+
const result = validateSpec(spec)
|
|
64
|
+
if (!result.valid) {
|
|
65
|
+
errors.push(...result.errors)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (errors.length === 0) {
|
|
69
|
+
console.log(chalk.green(`✓ Valid pipeline: ${spec.name}`))
|
|
70
|
+
console.log(chalk.dim(` ${spec.nodes.length} node(s), ${spec.edges.length} edge(s)`))
|
|
71
|
+
} else {
|
|
72
|
+
console.error(chalk.red('✗ Invalid pipeline:'))
|
|
73
|
+
for (const err of errors) {
|
|
74
|
+
console.error(chalk.red(` • ${err}`))
|
|
75
|
+
}
|
|
76
|
+
process.exit(1)
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Shazron Abdullah
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import os from 'os'
|
|
18
|
+
import path from 'path'
|
|
19
|
+
import { mkdirSync, rmSync } from 'fs'
|
|
20
|
+
import chalk from 'chalk'
|
|
21
|
+
import PQueue from 'p-queue'
|
|
22
|
+
import { topoSort } from './topoSort.js'
|
|
23
|
+
import { handleVideoCutter } from './nodeHandlers/video-cutter.js'
|
|
24
|
+
import { handleVideoStitcher } from './nodeHandlers/video-stitcher.js'
|
|
25
|
+
import { handleInputFile } from './nodeHandlers/input-file.js'
|
|
26
|
+
import { handleInputFolder } from './nodeHandlers/input-folder.js'
|
|
27
|
+
|
|
28
|
+
const HANDLERS = {
|
|
29
|
+
'video-cutter': handleVideoCutter,
|
|
30
|
+
'video-stitcher': handleVideoStitcher,
|
|
31
|
+
'output-folder': async () => {}, // resolved during cutter/stitcher execution
|
|
32
|
+
'input-file': handleInputFile,
|
|
33
|
+
'input-folder': handleInputFolder
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Executes a validated pipeline spec.
|
|
38
|
+
*
|
|
39
|
+
* @param {object} spec - parsed pipeline spec
|
|
40
|
+
* @param {{ keepTemp?: boolean, dryRun?: boolean }} opts
|
|
41
|
+
*/
|
|
42
|
+
export async function executePipeline(spec, opts = {}) {
|
|
43
|
+
const { keepTemp = false, dryRun = false, overwrite = false } = opts
|
|
44
|
+
|
|
45
|
+
const levels = topoSort(spec.nodes, spec.edges)
|
|
46
|
+
|
|
47
|
+
const tempRoot = path.join(
|
|
48
|
+
os.tmpdir(),
|
|
49
|
+
'video-pipeline',
|
|
50
|
+
`${spec.name}-${Date.now()}`
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
if (!dryRun) {
|
|
54
|
+
mkdirSync(tempRoot, { recursive: true })
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
console.log(chalk.bold(`\nPipeline: ${spec.name}`))
|
|
58
|
+
console.log(chalk.dim(` ${spec.nodes.length} node(s) across ${levels.length} level(s)`))
|
|
59
|
+
if (dryRun) console.log(chalk.yellow(' [dry-run mode]\n'))
|
|
60
|
+
|
|
61
|
+
// context stores runtime outputs keyed by nodeId
|
|
62
|
+
const context = new Map()
|
|
63
|
+
|
|
64
|
+
// Build a nodeId -> node map and edge lookup
|
|
65
|
+
const nodeMap = new Map(spec.nodes.map((n) => [n.id, n]))
|
|
66
|
+
|
|
67
|
+
for (let i = 0; i < levels.length; i++) {
|
|
68
|
+
const level = levels[i]
|
|
69
|
+
console.log(chalk.dim(`\nLevel ${i + 1}: ${level.join(', ')}`))
|
|
70
|
+
|
|
71
|
+
const queue = new PQueue({ concurrency: os.cpus().length })
|
|
72
|
+
|
|
73
|
+
const tasks = level.map((nodeId) =>
|
|
74
|
+
queue.add(async () => {
|
|
75
|
+
const node = nodeMap.get(nodeId)
|
|
76
|
+
const handler = HANDLERS[node.type]
|
|
77
|
+
|
|
78
|
+
if (!handler) {
|
|
79
|
+
throw new Error(`No handler for node type: ${node.type}`)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Find edges where this node is the target
|
|
83
|
+
const incomingEdges = spec.edges.filter((e) => e.target === nodeId)
|
|
84
|
+
|
|
85
|
+
if (node.type === 'video-stitcher') {
|
|
86
|
+
// Collect output-folder paths from connected output-folder nodes
|
|
87
|
+
const outputFolderPaths = spec.edges
|
|
88
|
+
.filter((e) => e.source === nodeId)
|
|
89
|
+
.map((e) => nodeMap.get(e.target))
|
|
90
|
+
.filter((n) => n?.type === 'output-folder' && n.config?.path)
|
|
91
|
+
.map((n) => n.config.path)
|
|
92
|
+
|
|
93
|
+
// If no output configured (and no output-folder node), derive default from the single cutter's input dir
|
|
94
|
+
let effectiveNode = node
|
|
95
|
+
if (!node.config.output && outputFolderPaths.length === 0) {
|
|
96
|
+
const cutters = spec.nodes.filter((n) => n.type === 'video-cutter')
|
|
97
|
+
if (cutters.length === 1 && cutters[0].config.input) {
|
|
98
|
+
const inputDir = path.dirname(cutters[0].config.input)
|
|
99
|
+
effectiveNode = { ...node, config: { ...node.config, output: path.join(inputDir, 'stitch-output') } }
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
await handler(effectiveNode, context, tempRoot, incomingEdges, { dryRun, overwrite, outputFolderPaths })
|
|
103
|
+
} else if (node.type === 'video-cutter') {
|
|
104
|
+
// Collect output-folder paths from connected output-folder nodes
|
|
105
|
+
const outputFolderPaths = spec.edges
|
|
106
|
+
.filter((e) => e.source === nodeId)
|
|
107
|
+
.map((e) => nodeMap.get(e.target))
|
|
108
|
+
.filter((n) => n?.type === 'output-folder' && n.config?.path)
|
|
109
|
+
.map((n) => n.config.path)
|
|
110
|
+
|
|
111
|
+
// Collect input files from connected input-file / input-folder nodes
|
|
112
|
+
const inputFiles = spec.edges
|
|
113
|
+
.filter((e) => e.target === nodeId)
|
|
114
|
+
.map((e) => nodeMap.get(e.source))
|
|
115
|
+
.filter((n) => n?.type === 'input-file' || n?.type === 'input-folder')
|
|
116
|
+
.flatMap((n) => context.get(n.id)?.outputs ?? [])
|
|
117
|
+
|
|
118
|
+
await handler(node, context, tempRoot, { dryRun, overwrite, outputFolderPaths, inputFiles })
|
|
119
|
+
} else {
|
|
120
|
+
await handler(node, context, tempRoot, { dryRun, overwrite })
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
await Promise.all(tasks)
|
|
126
|
+
await queue.onIdle()
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Print final outputs
|
|
130
|
+
console.log(chalk.green('\n✓ Pipeline complete\n'))
|
|
131
|
+
for (const [nodeId, ctx] of context) {
|
|
132
|
+
if (ctx.outputs?.length) {
|
|
133
|
+
for (const f of ctx.outputs) {
|
|
134
|
+
console.log(chalk.green(` Output [${nodeId}]: ${f}`))
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!dryRun && !keepTemp) {
|
|
140
|
+
try {
|
|
141
|
+
rmSync(tempRoot, { recursive: true, force: true })
|
|
142
|
+
} catch {
|
|
143
|
+
// Non-fatal: temp cleanup failure
|
|
144
|
+
}
|
|
145
|
+
} else if (!dryRun && keepTemp) {
|
|
146
|
+
console.log(chalk.dim(`\n Temp files: ${tempRoot}`))
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Shazron Abdullah
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import os from 'os'
|
|
18
|
+
import path from 'path'
|
|
19
|
+
|
|
20
|
+
function expandPath(p) {
|
|
21
|
+
return p.startsWith('~/') ? path.join(os.homedir(), p.slice(2)) : p
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Resolves the configured file path and stores it as a single-element outputs array.
|
|
26
|
+
*
|
|
27
|
+
* @param {object} node - the spec node
|
|
28
|
+
* @param {Map} context - runtime context keyed by nodeId
|
|
29
|
+
* @param {string} _tempRoot - unused
|
|
30
|
+
* @param {object} opts - { dryRun }
|
|
31
|
+
*/
|
|
32
|
+
export async function handleInputFile(node, context, _tempRoot, opts = {}) {
|
|
33
|
+
const { config } = node
|
|
34
|
+
const filePath = config.path ? expandPath(config.path) : null
|
|
35
|
+
|
|
36
|
+
if (!filePath && !opts.dryRun) {
|
|
37
|
+
throw new Error(`Node "${node.id}" (input-file): no file path configured`)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
context.set(node.id, { outputs: filePath ? [filePath] : [] })
|
|
41
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Shazron Abdullah
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import os from 'os'
|
|
18
|
+
import path from 'path'
|
|
19
|
+
import { glob } from 'glob'
|
|
20
|
+
import chalk from 'chalk'
|
|
21
|
+
|
|
22
|
+
function expandPath(p) {
|
|
23
|
+
return p.startsWith('~/') ? path.join(os.homedir(), p.slice(2)) : p
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Globs the configured folder using the optional filter pattern and stores
|
|
28
|
+
* all matching file paths as the outputs array.
|
|
29
|
+
*
|
|
30
|
+
* @param {object} node - the spec node
|
|
31
|
+
* @param {Map} context - runtime context keyed by nodeId
|
|
32
|
+
* @param {string} _tempRoot - unused
|
|
33
|
+
* @param {object} opts - { dryRun }
|
|
34
|
+
*/
|
|
35
|
+
export async function handleInputFolder(node, context, _tempRoot, opts = {}) {
|
|
36
|
+
const { config } = node
|
|
37
|
+
const folderPath = config.path ? expandPath(config.path) : null
|
|
38
|
+
|
|
39
|
+
if (!folderPath && !opts.dryRun) {
|
|
40
|
+
throw new Error(`Node "${node.id}" (input-folder): no folder path configured`)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (opts.dryRun || !folderPath) {
|
|
44
|
+
context.set(node.id, { outputs: [] })
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Use the user-supplied filter as a glob pattern; default to all files (non-recursive top level)
|
|
49
|
+
const pattern = config.filter?.trim() || '*'
|
|
50
|
+
const files = (await glob(pattern, { cwd: folderPath, absolute: true, nodir: true })).sort()
|
|
51
|
+
|
|
52
|
+
console.log(chalk.dim(` Input folder: ${folderPath} — ${files.length} file(s) matched`))
|
|
53
|
+
|
|
54
|
+
context.set(node.id, { outputs: files })
|
|
55
|
+
}
|