@nodetool-ai/base-nodes 0.7.0-rc.22 → 0.8.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/README.md +21 -0
- package/dist/.nodetool-build-stamp +1 -0
- package/dist/index.d.ts +61 -67
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +179 -173
- package/dist/index.js.map +1 -1
- package/dist/platforms/browser.d.ts +19 -0
- package/dist/platforms/browser.d.ts.map +1 -0
- package/dist/platforms/browser.js +29 -0
- package/dist/platforms/browser.js.map +1 -0
- package/package.json +53 -65
- package/dist/lib/audio-wav.d.ts +0 -66
- package/dist/lib/audio-wav.d.ts.map +0 -1
- package/dist/lib/audio-wav.js +0 -215
- package/dist/lib/audio-wav.js.map +0 -1
- package/dist/nodes/agent-tool-hydration.d.ts +0 -10
- package/dist/nodes/agent-tool-hydration.d.ts.map +0 -1
- package/dist/nodes/agent-tool-hydration.js +0 -43
- package/dist/nodes/agent-tool-hydration.js.map +0 -1
- package/dist/nodes/agents.d.ts +0 -262
- package/dist/nodes/agents.d.ts.map +0 -1
- package/dist/nodes/agents.js +0 -2856
- package/dist/nodes/agents.js.map +0 -1
- package/dist/nodes/anthropic.d.ts +0 -26
- package/dist/nodes/anthropic.d.ts.map +0 -1
- package/dist/nodes/anthropic.js +0 -133
- package/dist/nodes/anthropic.js.map +0 -1
- package/dist/nodes/apify.d.ts +0 -120
- package/dist/nodes/apify.d.ts.map +0 -1
- package/dist/nodes/apify.js +0 -618
- package/dist/nodes/apify.js.map +0 -1
- package/dist/nodes/audio.d.ts +0 -301
- package/dist/nodes/audio.d.ts.map +0 -1
- package/dist/nodes/audio.js +0 -1274
- package/dist/nodes/audio.js.map +0 -1
- package/dist/nodes/code-node.d.ts +0 -34
- package/dist/nodes/code-node.d.ts.map +0 -1
- package/dist/nodes/code-node.js +0 -343
- package/dist/nodes/code-node.js.map +0 -1
- package/dist/nodes/code.d.ts +0 -301
- package/dist/nodes/code.d.ts.map +0 -1
- package/dist/nodes/code.js +0 -850
- package/dist/nodes/code.js.map +0 -1
- package/dist/nodes/compare.d.ts +0 -18
- package/dist/nodes/compare.d.ts.map +0 -1
- package/dist/nodes/compare.js +0 -122
- package/dist/nodes/compare.js.map +0 -1
- package/dist/nodes/constant.d.ts +0 -315
- package/dist/nodes/constant.d.ts.map +0 -1
- package/dist/nodes/constant.js +0 -715
- package/dist/nodes/constant.js.map +0 -1
- package/dist/nodes/control.d.ts +0 -83
- package/dist/nodes/control.d.ts.map +0 -1
- package/dist/nodes/control.js +0 -212
- package/dist/nodes/control.js.map +0 -1
- package/dist/nodes/data.d.ts +0 -369
- package/dist/nodes/data.d.ts.map +0 -1
- package/dist/nodes/data.js +0 -1590
- package/dist/nodes/data.js.map +0 -1
- package/dist/nodes/document.d.ts +0 -134
- package/dist/nodes/document.d.ts.map +0 -1
- package/dist/nodes/document.js +0 -984
- package/dist/nodes/document.js.map +0 -1
- package/dist/nodes/extended-placeholders.d.ts +0 -6
- package/dist/nodes/extended-placeholders.d.ts.map +0 -1
- package/dist/nodes/extended-placeholders.js +0 -19
- package/dist/nodes/extended-placeholders.js.map +0 -1
- package/dist/nodes/gemini.d.ts +0 -110
- package/dist/nodes/gemini.d.ts.map +0 -1
- package/dist/nodes/gemini.js +0 -871
- package/dist/nodes/gemini.js.map +0 -1
- package/dist/nodes/generators.d.ts +0 -84
- package/dist/nodes/generators.d.ts.map +0 -1
- package/dist/nodes/generators.js +0 -968
- package/dist/nodes/generators.js.map +0 -1
- package/dist/nodes/image.d.ts +0 -232
- package/dist/nodes/image.d.ts.map +0 -1
- package/dist/nodes/image.js +0 -1395
- package/dist/nodes/image.js.map +0 -1
- package/dist/nodes/input.d.ts +0 -445
- package/dist/nodes/input.d.ts.map +0 -1
- package/dist/nodes/input.js +0 -1459
- package/dist/nodes/input.js.map +0 -1
- package/dist/nodes/kie-dynamic.d.ts +0 -31
- package/dist/nodes/kie-dynamic.d.ts.map +0 -1
- package/dist/nodes/kie-dynamic.js +0 -270
- package/dist/nodes/kie-dynamic.js.map +0 -1
- package/dist/nodes/lib-audio-dsp.d.ts +0 -85
- package/dist/nodes/lib-audio-dsp.d.ts.map +0 -1
- package/dist/nodes/lib-audio-dsp.js +0 -450
- package/dist/nodes/lib-audio-dsp.js.map +0 -1
- package/dist/nodes/lib-audio-effects.d.ts +0 -116
- package/dist/nodes/lib-audio-effects.d.ts.map +0 -1
- package/dist/nodes/lib-audio-effects.js +0 -905
- package/dist/nodes/lib-audio-effects.js.map +0 -1
- package/dist/nodes/lib-browser.d.ts +0 -99
- package/dist/nodes/lib-browser.d.ts.map +0 -1
- package/dist/nodes/lib-browser.js +0 -692
- package/dist/nodes/lib-browser.js.map +0 -1
- package/dist/nodes/lib-charts.d.ts +0 -20
- package/dist/nodes/lib-charts.d.ts.map +0 -1
- package/dist/nodes/lib-charts.js +0 -245
- package/dist/nodes/lib-charts.js.map +0 -1
- package/dist/nodes/lib-datetime.d.ts +0 -108
- package/dist/nodes/lib-datetime.d.ts.map +0 -1
- package/dist/nodes/lib-datetime.js +0 -421
- package/dist/nodes/lib-datetime.js.map +0 -1
- package/dist/nodes/lib-doc-convert.d.ts +0 -19
- package/dist/nodes/lib-doc-convert.d.ts.map +0 -1
- package/dist/nodes/lib-doc-convert.js +0 -257
- package/dist/nodes/lib-doc-convert.js.map +0 -1
- package/dist/nodes/lib-doc-transform.d.ts +0 -39
- package/dist/nodes/lib-doc-transform.d.ts.map +0 -1
- package/dist/nodes/lib-doc-transform.js +0 -335
- package/dist/nodes/lib-doc-transform.js.map +0 -1
- package/dist/nodes/lib-docx.d.ts +0 -106
- package/dist/nodes/lib-docx.d.ts.map +0 -1
- package/dist/nodes/lib-docx.js +0 -595
- package/dist/nodes/lib-docx.js.map +0 -1
- package/dist/nodes/lib-epub.d.ts +0 -55
- package/dist/nodes/lib-epub.d.ts.map +0 -1
- package/dist/nodes/lib-epub.js +0 -220
- package/dist/nodes/lib-epub.js.map +0 -1
- package/dist/nodes/lib-excel.d.ts +0 -74
- package/dist/nodes/lib-excel.d.ts.map +0 -1
- package/dist/nodes/lib-excel.js +0 -448
- package/dist/nodes/lib-excel.js.map +0 -1
- package/dist/nodes/lib-graphql.d.ts +0 -66
- package/dist/nodes/lib-graphql.d.ts.map +0 -1
- package/dist/nodes/lib-graphql.js +0 -333
- package/dist/nodes/lib-graphql.js.map +0 -1
- package/dist/nodes/lib-grid.d.ts +0 -27
- package/dist/nodes/lib-grid.d.ts.map +0 -1
- package/dist/nodes/lib-grid.js +0 -172
- package/dist/nodes/lib-grid.js.map +0 -1
- package/dist/nodes/lib-html-parse.d.ts +0 -108
- package/dist/nodes/lib-html-parse.d.ts.map +0 -1
- package/dist/nodes/lib-html-parse.js +0 -365
- package/dist/nodes/lib-html-parse.js.map +0 -1
- package/dist/nodes/lib-http.d.ts +0 -98
- package/dist/nodes/lib-http.d.ts.map +0 -1
- package/dist/nodes/lib-http.js +0 -267
- package/dist/nodes/lib-http.js.map +0 -1
- package/dist/nodes/lib-image-color-grading.d.ts +0 -3
- package/dist/nodes/lib-image-color-grading.d.ts.map +0 -1
- package/dist/nodes/lib-image-color-grading.js +0 -1386
- package/dist/nodes/lib-image-color-grading.js.map +0 -1
- package/dist/nodes/lib-image-draw.d.ts +0 -3
- package/dist/nodes/lib-image-draw.d.ts.map +0 -1
- package/dist/nodes/lib-image-draw.js +0 -470
- package/dist/nodes/lib-image-draw.js.map +0 -1
- package/dist/nodes/lib-image-enhance.d.ts +0 -3
- package/dist/nodes/lib-image-enhance.d.ts.map +0 -1
- package/dist/nodes/lib-image-enhance.js +0 -731
- package/dist/nodes/lib-image-enhance.js.map +0 -1
- package/dist/nodes/lib-image-filter.d.ts +0 -3
- package/dist/nodes/lib-image-filter.d.ts.map +0 -1
- package/dist/nodes/lib-image-filter.js +0 -684
- package/dist/nodes/lib-image-filter.js.map +0 -1
- package/dist/nodes/lib-image-utils.d.ts +0 -24
- package/dist/nodes/lib-image-utils.d.ts.map +0 -1
- package/dist/nodes/lib-image-utils.js +0 -150
- package/dist/nodes/lib-image-utils.js.map +0 -1
- package/dist/nodes/lib-mail.d.ts +0 -74
- package/dist/nodes/lib-mail.d.ts.map +0 -1
- package/dist/nodes/lib-mail.js +0 -452
- package/dist/nodes/lib-mail.js.map +0 -1
- package/dist/nodes/lib-markdown.d.ts +0 -70
- package/dist/nodes/lib-markdown.d.ts.map +0 -1
- package/dist/nodes/lib-markdown.js +0 -236
- package/dist/nodes/lib-markdown.js.map +0 -1
- package/dist/nodes/lib-nlp.d.ts +0 -98
- package/dist/nodes/lib-nlp.d.ts.map +0 -1
- package/dist/nodes/lib-nlp.js +0 -374
- package/dist/nodes/lib-nlp.js.map +0 -1
- package/dist/nodes/lib-notion.d.ts +0 -99
- package/dist/nodes/lib-notion.d.ts.map +0 -1
- package/dist/nodes/lib-notion.js +0 -601
- package/dist/nodes/lib-notion.js.map +0 -1
- package/dist/nodes/lib-ocr.d.ts +0 -31
- package/dist/nodes/lib-ocr.d.ts.map +0 -1
- package/dist/nodes/lib-ocr.js +0 -143
- package/dist/nodes/lib-ocr.js.map +0 -1
- package/dist/nodes/lib-os.d.ts +0 -312
- package/dist/nodes/lib-os.d.ts.map +0 -1
- package/dist/nodes/lib-os.js +0 -909
- package/dist/nodes/lib-os.js.map +0 -1
- package/dist/nodes/lib-pdf.d.ts +0 -142
- package/dist/nodes/lib-pdf.d.ts.map +0 -1
- package/dist/nodes/lib-pdf.js +0 -871
- package/dist/nodes/lib-pdf.js.map +0 -1
- package/dist/nodes/lib-pptx.d.ts +0 -29
- package/dist/nodes/lib-pptx.d.ts.map +0 -1
- package/dist/nodes/lib-pptx.js +0 -122
- package/dist/nodes/lib-pptx.js.map +0 -1
- package/dist/nodes/lib-rss.d.ts +0 -31
- package/dist/nodes/lib-rss.d.ts.map +0 -1
- package/dist/nodes/lib-rss.js +0 -163
- package/dist/nodes/lib-rss.js.map +0 -1
- package/dist/nodes/lib-s3.d.ts +0 -112
- package/dist/nodes/lib-s3.d.ts.map +0 -1
- package/dist/nodes/lib-s3.js +0 -521
- package/dist/nodes/lib-s3.js.map +0 -1
- package/dist/nodes/lib-secret.d.ts +0 -15
- package/dist/nodes/lib-secret.d.ts.map +0 -1
- package/dist/nodes/lib-secret.js +0 -42
- package/dist/nodes/lib-secret.js.map +0 -1
- package/dist/nodes/lib-sqlite.d.ts +0 -102
- package/dist/nodes/lib-sqlite.d.ts.map +0 -1
- package/dist/nodes/lib-sqlite.js +0 -532
- package/dist/nodes/lib-sqlite.js.map +0 -1
- package/dist/nodes/lib-supabase.d.ts +0 -86
- package/dist/nodes/lib-supabase.d.ts.map +0 -1
- package/dist/nodes/lib-supabase.js +0 -425
- package/dist/nodes/lib-supabase.js.map +0 -1
- package/dist/nodes/lib-svg.d.ts +0 -199
- package/dist/nodes/lib-svg.d.ts.map +0 -1
- package/dist/nodes/lib-svg.js +0 -1027
- package/dist/nodes/lib-svg.js.map +0 -1
- package/dist/nodes/lib-tensorflow.d.ts +0 -71
- package/dist/nodes/lib-tensorflow.d.ts.map +0 -1
- package/dist/nodes/lib-tensorflow.js +0 -351
- package/dist/nodes/lib-tensorflow.js.map +0 -1
- package/dist/nodes/lib-twilio.d.ts +0 -71
- package/dist/nodes/lib-twilio.d.ts.map +0 -1
- package/dist/nodes/lib-twilio.js +0 -469
- package/dist/nodes/lib-twilio.js.map +0 -1
- package/dist/nodes/lib-validate.d.ts +0 -84
- package/dist/nodes/lib-validate.d.ts.map +0 -1
- package/dist/nodes/lib-validate.js +0 -215
- package/dist/nodes/lib-validate.js.map +0 -1
- package/dist/nodes/lib-video-download.d.ts +0 -26
- package/dist/nodes/lib-video-download.d.ts.map +0 -1
- package/dist/nodes/lib-video-download.js +0 -299
- package/dist/nodes/lib-video-download.js.map +0 -1
- package/dist/nodes/messaging.d.ts +0 -88
- package/dist/nodes/messaging.d.ts.map +0 -1
- package/dist/nodes/messaging.js +0 -418
- package/dist/nodes/messaging.js.map +0 -1
- package/dist/nodes/mistral.d.ts +0 -83
- package/dist/nodes/mistral.d.ts.map +0 -1
- package/dist/nodes/mistral.js +0 -450
- package/dist/nodes/mistral.js.map +0 -1
- package/dist/nodes/model3d/analysis.d.ts +0 -13
- package/dist/nodes/model3d/analysis.d.ts.map +0 -1
- package/dist/nodes/model3d/analysis.js +0 -36
- package/dist/nodes/model3d/analysis.js.map +0 -1
- package/dist/nodes/model3d/base.d.ts +0 -11
- package/dist/nodes/model3d/base.d.ts.map +0 -1
- package/dist/nodes/model3d/base.js +0 -20
- package/dist/nodes/model3d/base.js.map +0 -1
- package/dist/nodes/model3d/boolean-ops.d.ts +0 -2
- package/dist/nodes/model3d/boolean-ops.d.ts.map +0 -1
- package/dist/nodes/model3d/boolean-ops.js +0 -172
- package/dist/nodes/model3d/boolean-ops.js.map +0 -1
- package/dist/nodes/model3d/cleanup.d.ts +0 -47
- package/dist/nodes/model3d/cleanup.d.ts.map +0 -1
- package/dist/nodes/model3d/cleanup.js +0 -136
- package/dist/nodes/model3d/cleanup.js.map +0 -1
- package/dist/nodes/model3d/convert.d.ts +0 -14
- package/dist/nodes/model3d/convert.d.ts.map +0 -1
- package/dist/nodes/model3d/convert.js +0 -64
- package/dist/nodes/model3d/convert.js.map +0 -1
- package/dist/nodes/model3d/defaults.d.ts +0 -34
- package/dist/nodes/model3d/defaults.d.ts.map +0 -1
- package/dist/nodes/model3d/defaults.js +0 -28
- package/dist/nodes/model3d/defaults.js.map +0 -1
- package/dist/nodes/model3d/document-ops.d.ts +0 -4
- package/dist/nodes/model3d/document-ops.d.ts.map +0 -1
- package/dist/nodes/model3d/document-ops.js +0 -40
- package/dist/nodes/model3d/document-ops.js.map +0 -1
- package/dist/nodes/model3d/generation.d.ts +0 -39
- package/dist/nodes/model3d/generation.d.ts.map +0 -1
- package/dist/nodes/model3d/generation.js +0 -179
- package/dist/nodes/model3d/generation.js.map +0 -1
- package/dist/nodes/model3d/glb.d.ts +0 -29
- package/dist/nodes/model3d/glb.d.ts.map +0 -1
- package/dist/nodes/model3d/glb.js +0 -255
- package/dist/nodes/model3d/glb.js.map +0 -1
- package/dist/nodes/model3d/index.d.ts +0 -20
- package/dist/nodes/model3d/index.d.ts.map +0 -1
- package/dist/nodes/model3d/index.js +0 -38
- package/dist/nodes/model3d/index.js.map +0 -1
- package/dist/nodes/model3d/io.d.ts +0 -38
- package/dist/nodes/model3d/io.d.ts.map +0 -1
- package/dist/nodes/model3d/io.js +0 -162
- package/dist/nodes/model3d/io.js.map +0 -1
- package/dist/nodes/model3d/mesh-ops.d.ts +0 -40
- package/dist/nodes/model3d/mesh-ops.d.ts.map +0 -1
- package/dist/nodes/model3d/mesh-ops.js +0 -775
- package/dist/nodes/model3d/mesh-ops.js.map +0 -1
- package/dist/nodes/model3d/nodes.d.ts +0 -2
- package/dist/nodes/model3d/nodes.d.ts.map +0 -1
- package/dist/nodes/model3d/nodes.js +0 -2
- package/dist/nodes/model3d/nodes.js.map +0 -1
- package/dist/nodes/model3d/ops.d.ts +0 -37
- package/dist/nodes/model3d/ops.d.ts.map +0 -1
- package/dist/nodes/model3d/ops.js +0 -137
- package/dist/nodes/model3d/ops.js.map +0 -1
- package/dist/nodes/model3d/transforms.d.ts +0 -49
- package/dist/nodes/model3d/transforms.d.ts.map +0 -1
- package/dist/nodes/model3d/transforms.js +0 -142
- package/dist/nodes/model3d/transforms.js.map +0 -1
- package/dist/nodes/model3d/types.d.ts +0 -81
- package/dist/nodes/model3d/types.d.ts.map +0 -1
- package/dist/nodes/model3d/types.js +0 -2
- package/dist/nodes/model3d/types.js.map +0 -1
- package/dist/nodes/model3d/utils.d.ts +0 -34
- package/dist/nodes/model3d/utils.d.ts.map +0 -1
- package/dist/nodes/model3d/utils.js +0 -185
- package/dist/nodes/model3d/utils.js.map +0 -1
- package/dist/nodes/model3d.d.ts +0 -2
- package/dist/nodes/model3d.d.ts.map +0 -1
- package/dist/nodes/model3d.js +0 -2
- package/dist/nodes/model3d.js.map +0 -1
- package/dist/nodes/openai.d.ts +0 -172
- package/dist/nodes/openai.d.ts.map +0 -1
- package/dist/nodes/openai.js +0 -1294
- package/dist/nodes/openai.js.map +0 -1
- package/dist/nodes/output.d.ts +0 -29
- package/dist/nodes/output.d.ts.map +0 -1
- package/dist/nodes/output.js +0 -87
- package/dist/nodes/output.js.map +0 -1
- package/dist/nodes/sandbox.d.ts +0 -63
- package/dist/nodes/sandbox.d.ts.map +0 -1
- package/dist/nodes/sandbox.js +0 -633
- package/dist/nodes/sandbox.js.map +0 -1
- package/dist/nodes/search.d.ts +0 -117
- package/dist/nodes/search.d.ts.map +0 -1
- package/dist/nodes/search.js +0 -607
- package/dist/nodes/search.js.map +0 -1
- package/dist/nodes/team.d.ts +0 -74
- package/dist/nodes/team.d.ts.map +0 -1
- package/dist/nodes/team.js +0 -309
- package/dist/nodes/team.js.map +0 -1
- package/dist/nodes/text-extra.d.ts +0 -633
- package/dist/nodes/text-extra.d.ts.map +0 -1
- package/dist/nodes/text-extra.js +0 -2226
- package/dist/nodes/text-extra.js.map +0 -1
- package/dist/nodes/tool-agents.d.ts +0 -334
- package/dist/nodes/tool-agents.d.ts.map +0 -1
- package/dist/nodes/tool-agents.js +0 -1861
- package/dist/nodes/tool-agents.js.map +0 -1
- package/dist/nodes/triggers.d.ts +0 -107
- package/dist/nodes/triggers.d.ts.map +0 -1
- package/dist/nodes/triggers.js +0 -706
- package/dist/nodes/triggers.js.map +0 -1
- package/dist/nodes/vector.d.ts +0 -168
- package/dist/nodes/vector.d.ts.map +0 -1
- package/dist/nodes/vector.js +0 -1121
- package/dist/nodes/vector.js.map +0 -1
- package/dist/nodes/video.d.ts +0 -387
- package/dist/nodes/video.d.ts.map +0 -1
- package/dist/nodes/video.js +0 -2383
- package/dist/nodes/video.js.map +0 -1
- package/dist/nodes/workflow.d.ts +0 -24
- package/dist/nodes/workflow.d.ts.map +0 -1
- package/dist/nodes/workflow.js +0 -146
- package/dist/nodes/workflow.js.map +0 -1
- package/dist/nodes/workspace.d.ts +0 -203
- package/dist/nodes/workspace.d.ts.map +0 -1
- package/dist/nodes/workspace.js +0 -795
- package/dist/nodes/workspace.js.map +0 -1
package/dist/nodes/video.js
DELETED
|
@@ -1,2383 +0,0 @@
|
|
|
1
|
-
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
-
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
-
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
-
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
-
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
-
};
|
|
7
|
-
import { BaseNode, prop } from "@nodetool-ai/node-sdk";
|
|
8
|
-
import { execFile as execFileCb } from "node:child_process";
|
|
9
|
-
import { promises as fs } from "node:fs";
|
|
10
|
-
import os from "node:os";
|
|
11
|
-
import path from "node:path";
|
|
12
|
-
import { fileURLToPath } from "node:url";
|
|
13
|
-
import { promisify } from "node:util";
|
|
14
|
-
import { audioBytes, toBytes } from "../lib/audio-wav.js";
|
|
15
|
-
const execFile = promisify(execFileCb);
|
|
16
|
-
function videoBytes(video) {
|
|
17
|
-
if (!video || typeof video !== "object")
|
|
18
|
-
return new Uint8Array();
|
|
19
|
-
return toBytes(video.data);
|
|
20
|
-
}
|
|
21
|
-
async function videoBytesAsync(video, context) {
|
|
22
|
-
if (!video || typeof video !== "object")
|
|
23
|
-
return new Uint8Array();
|
|
24
|
-
const ref = video;
|
|
25
|
-
if (ref.data)
|
|
26
|
-
return toBytes(ref.data);
|
|
27
|
-
if (typeof ref.uri === "string" && ref.uri) {
|
|
28
|
-
if (context?.storage) {
|
|
29
|
-
const stored = await context.storage.retrieve(ref.uri);
|
|
30
|
-
if (stored !== null)
|
|
31
|
-
return new Uint8Array(stored);
|
|
32
|
-
}
|
|
33
|
-
if (ref.uri.startsWith("file://")) {
|
|
34
|
-
return new Uint8Array(await fs.readFile(filePath(ref.uri)));
|
|
35
|
-
}
|
|
36
|
-
if (ref.uri.startsWith("http://") || ref.uri.startsWith("https://")) {
|
|
37
|
-
const response = await fetch(ref.uri);
|
|
38
|
-
return new Uint8Array(await response.arrayBuffer());
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
return new Uint8Array();
|
|
42
|
-
}
|
|
43
|
-
async function imageBytesAsync(image, context) {
|
|
44
|
-
if (!image || typeof image !== "object")
|
|
45
|
-
return new Uint8Array();
|
|
46
|
-
const ref = image;
|
|
47
|
-
if (ref.data)
|
|
48
|
-
return toBytes(ref.data);
|
|
49
|
-
if (typeof ref.uri === "string" && ref.uri) {
|
|
50
|
-
const uri = ref.uri;
|
|
51
|
-
const dataUriMatch = uri.match(/^data:[^;]*;base64,(.+)$/s);
|
|
52
|
-
if (dataUriMatch) {
|
|
53
|
-
return new Uint8Array(Buffer.from(dataUriMatch[1], "base64"));
|
|
54
|
-
}
|
|
55
|
-
if (context?.storage) {
|
|
56
|
-
const stored = await context.storage.retrieve(uri);
|
|
57
|
-
if (stored !== null)
|
|
58
|
-
return new Uint8Array(stored);
|
|
59
|
-
}
|
|
60
|
-
if (uri.startsWith("file://")) {
|
|
61
|
-
return new Uint8Array(await fs.readFile(filePath(uri)));
|
|
62
|
-
}
|
|
63
|
-
if (uri.startsWith("http://") || uri.startsWith("https://")) {
|
|
64
|
-
const response = await fetch(uri);
|
|
65
|
-
return new Uint8Array(await response.arrayBuffer());
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
return new Uint8Array();
|
|
69
|
-
}
|
|
70
|
-
function filePath(uriOrPath) {
|
|
71
|
-
if (uriOrPath.startsWith("file://")) {
|
|
72
|
-
try {
|
|
73
|
-
return fileURLToPath(new URL(uriOrPath));
|
|
74
|
-
}
|
|
75
|
-
catch {
|
|
76
|
-
// Fallback for non-standard URIs like file://C:\path
|
|
77
|
-
return uriOrPath.slice("file://".length);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
return uriOrPath;
|
|
81
|
-
}
|
|
82
|
-
function dateName(name) {
|
|
83
|
-
const now = new Date();
|
|
84
|
-
const pad = (v) => String(v).padStart(2, "0");
|
|
85
|
-
return name
|
|
86
|
-
.replaceAll("%Y", String(now.getFullYear()))
|
|
87
|
-
.replaceAll("%m", pad(now.getMonth() + 1))
|
|
88
|
-
.replaceAll("%d", pad(now.getDate()))
|
|
89
|
-
.replaceAll("%H", pad(now.getHours()))
|
|
90
|
-
.replaceAll("%M", pad(now.getMinutes()))
|
|
91
|
-
.replaceAll("%S", pad(now.getSeconds()));
|
|
92
|
-
}
|
|
93
|
-
function bytesConcat(parts) {
|
|
94
|
-
const total = parts.reduce((s, p) => s + p.length, 0);
|
|
95
|
-
const out = new Uint8Array(total);
|
|
96
|
-
let off = 0;
|
|
97
|
-
for (const p of parts) {
|
|
98
|
-
out.set(p, off);
|
|
99
|
-
off += p.length;
|
|
100
|
-
}
|
|
101
|
-
return out;
|
|
102
|
-
}
|
|
103
|
-
async function ffprobeDuration(filePath) {
|
|
104
|
-
try {
|
|
105
|
-
const { stdout } = await execFile("ffprobe", [
|
|
106
|
-
"-v",
|
|
107
|
-
"error",
|
|
108
|
-
"-show_entries",
|
|
109
|
-
"format=duration",
|
|
110
|
-
"-of",
|
|
111
|
-
"default=noprint_wrappers=1:nokey=1",
|
|
112
|
-
filePath
|
|
113
|
-
]);
|
|
114
|
-
const val = parseFloat(stdout.trim());
|
|
115
|
-
return isNaN(val) ? 0 : val;
|
|
116
|
-
}
|
|
117
|
-
catch {
|
|
118
|
-
return 0;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
function videoRef(data, extras = {}) {
|
|
122
|
-
return {
|
|
123
|
-
type: "video",
|
|
124
|
-
data: Buffer.from(data).toString("base64"),
|
|
125
|
-
...extras
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
function modelConfig(props) {
|
|
129
|
-
const model = (props.model ?? {});
|
|
130
|
-
return {
|
|
131
|
-
providerId: typeof model.provider === "string" ? model.provider : "",
|
|
132
|
-
modelId: typeof model.id === "string" ? model.id : ""
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
function canUseProvider(context, providerId, modelId) {
|
|
136
|
-
return (!!context &&
|
|
137
|
-
typeof context.runProviderPrediction === "function" &&
|
|
138
|
-
!!providerId &&
|
|
139
|
-
!!modelId);
|
|
140
|
-
}
|
|
141
|
-
const VIDEO_ASPECT_RATIO_VALUES = [
|
|
142
|
-
"21:9",
|
|
143
|
-
"16:9",
|
|
144
|
-
"4:3",
|
|
145
|
-
"1:1",
|
|
146
|
-
"9:16",
|
|
147
|
-
"3:4"
|
|
148
|
-
];
|
|
149
|
-
const VIDEO_RESOLUTION_VALUES = ["720p", "1080p", "1440p", "4K"];
|
|
150
|
-
const VIDEO_DURATION_VALUES = [2, 3, 4, 5, 6, 8];
|
|
151
|
-
async function withTempFile(suffix, bytes) {
|
|
152
|
-
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "nodetool-video-"));
|
|
153
|
-
const file = path.join(dir, `input${suffix}`);
|
|
154
|
-
await fs.writeFile(file, bytes);
|
|
155
|
-
return {
|
|
156
|
-
path: file,
|
|
157
|
-
cleanup: async () => {
|
|
158
|
-
await fs.rm(dir, { recursive: true, force: true });
|
|
159
|
-
}
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
async function ffmpegTransform(video, args, extraInputs = []) {
|
|
163
|
-
if (video.length === 0)
|
|
164
|
-
return new Uint8Array();
|
|
165
|
-
const mainInput = await withTempFile(".mp4", video);
|
|
166
|
-
const others = await Promise.all(extraInputs.map((item) => withTempFile(item.suffix, item.bytes)));
|
|
167
|
-
const outputDir = await fs.mkdtemp(path.join(os.tmpdir(), "nodetool-video-out-"));
|
|
168
|
-
const outputPath = path.join(outputDir, "output.mp4");
|
|
169
|
-
try {
|
|
170
|
-
const inputArgs = ["-y", "-i", mainInput.path];
|
|
171
|
-
for (const other of others)
|
|
172
|
-
inputArgs.push("-i", other.path);
|
|
173
|
-
await execFile("ffmpeg", [...inputArgs, ...args, outputPath], {
|
|
174
|
-
maxBuffer: 10 * 1024 * 1024
|
|
175
|
-
});
|
|
176
|
-
return new Uint8Array(await fs.readFile(outputPath));
|
|
177
|
-
}
|
|
178
|
-
catch {
|
|
179
|
-
return null;
|
|
180
|
-
}
|
|
181
|
-
finally {
|
|
182
|
-
await mainInput.cleanup();
|
|
183
|
-
for (const other of others)
|
|
184
|
-
await other.cleanup();
|
|
185
|
-
await fs.rm(outputDir, { recursive: true, force: true });
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
export class TextToVideoNode extends BaseNode {
|
|
189
|
-
static nodeType = "nodetool.video.TextToVideo";
|
|
190
|
-
static title = "Text To Video";
|
|
191
|
-
static description = "Generate videos from text prompts using any supported video provider. Automatically routes to the appropriate backend (Gemini Veo, HuggingFace).\n video, generation, AI, text-to-video, t2v";
|
|
192
|
-
static metadataOutputTypes = {
|
|
193
|
-
output: "video"
|
|
194
|
-
};
|
|
195
|
-
static basicFields = [
|
|
196
|
-
"model",
|
|
197
|
-
"prompt",
|
|
198
|
-
"aspect_ratio",
|
|
199
|
-
"resolution",
|
|
200
|
-
"duration"
|
|
201
|
-
];
|
|
202
|
-
static exposeAsTool = true;
|
|
203
|
-
async process(context) {
|
|
204
|
-
const text = String(this.prompt ?? "");
|
|
205
|
-
const { providerId, modelId } = modelConfig(this.serialize());
|
|
206
|
-
if (canUseProvider(context, providerId, modelId)) {
|
|
207
|
-
const output = (await context.runProviderPrediction({
|
|
208
|
-
provider: providerId,
|
|
209
|
-
capability: "text_to_video",
|
|
210
|
-
model: modelId,
|
|
211
|
-
params: {
|
|
212
|
-
prompt: text,
|
|
213
|
-
negative_prompt: this.negative_prompt,
|
|
214
|
-
duration_seconds: Number(this.duration ?? 0) || undefined,
|
|
215
|
-
aspect_ratio: this.aspect_ratio,
|
|
216
|
-
resolution: this.resolution
|
|
217
|
-
}
|
|
218
|
-
}));
|
|
219
|
-
return { output: videoRef(output) };
|
|
220
|
-
}
|
|
221
|
-
const bytes = Uint8Array.from(Buffer.from(text, "utf8"));
|
|
222
|
-
return { output: videoRef(bytes) };
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
__decorate([
|
|
226
|
-
prop({
|
|
227
|
-
type: "video_model",
|
|
228
|
-
default: {
|
|
229
|
-
type: "video_model",
|
|
230
|
-
provider: "gemini",
|
|
231
|
-
id: "veo-3.1-generate-preview",
|
|
232
|
-
name: "Veo 3.1 Preview",
|
|
233
|
-
path: null,
|
|
234
|
-
supported_tasks: []
|
|
235
|
-
},
|
|
236
|
-
title: "Model",
|
|
237
|
-
description: "The video generation model to use"
|
|
238
|
-
})
|
|
239
|
-
], TextToVideoNode.prototype, "model", void 0);
|
|
240
|
-
__decorate([
|
|
241
|
-
prop({
|
|
242
|
-
type: "str",
|
|
243
|
-
default: "A cat playing with a ball of yarn",
|
|
244
|
-
title: "Prompt",
|
|
245
|
-
description: "Text prompt describing the desired video"
|
|
246
|
-
})
|
|
247
|
-
], TextToVideoNode.prototype, "prompt", void 0);
|
|
248
|
-
__decorate([
|
|
249
|
-
prop({
|
|
250
|
-
type: "str",
|
|
251
|
-
default: "",
|
|
252
|
-
title: "Negative Prompt",
|
|
253
|
-
description: "Text prompt describing what to avoid in the video"
|
|
254
|
-
})
|
|
255
|
-
], TextToVideoNode.prototype, "negative_prompt", void 0);
|
|
256
|
-
__decorate([
|
|
257
|
-
prop({
|
|
258
|
-
type: "str",
|
|
259
|
-
default: "16:9",
|
|
260
|
-
title: "Aspect Ratio",
|
|
261
|
-
description: "Aspect ratio for the video",
|
|
262
|
-
values: VIDEO_ASPECT_RATIO_VALUES,
|
|
263
|
-
json_schema_extra: { type: "media_aspect_ratio_video" }
|
|
264
|
-
})
|
|
265
|
-
], TextToVideoNode.prototype, "aspect_ratio", void 0);
|
|
266
|
-
__decorate([
|
|
267
|
-
prop({
|
|
268
|
-
type: "str",
|
|
269
|
-
default: "1080p",
|
|
270
|
-
title: "Resolution",
|
|
271
|
-
description: "Video resolution",
|
|
272
|
-
values: VIDEO_RESOLUTION_VALUES,
|
|
273
|
-
json_schema_extra: { type: "media_resolution_video" }
|
|
274
|
-
})
|
|
275
|
-
], TextToVideoNode.prototype, "resolution", void 0);
|
|
276
|
-
__decorate([
|
|
277
|
-
prop({
|
|
278
|
-
type: "int",
|
|
279
|
-
default: 8,
|
|
280
|
-
title: "Duration",
|
|
281
|
-
description: "Video duration in seconds",
|
|
282
|
-
values: VIDEO_DURATION_VALUES,
|
|
283
|
-
json_schema_extra: { type: "media_duration" }
|
|
284
|
-
})
|
|
285
|
-
], TextToVideoNode.prototype, "duration", void 0);
|
|
286
|
-
__decorate([
|
|
287
|
-
prop({
|
|
288
|
-
type: "int",
|
|
289
|
-
default: 0,
|
|
290
|
-
title: "Timeout Seconds",
|
|
291
|
-
description: "Timeout in seconds for API calls (0 = use provider default)",
|
|
292
|
-
min: 0,
|
|
293
|
-
max: 7200
|
|
294
|
-
})
|
|
295
|
-
], TextToVideoNode.prototype, "timeout_seconds", void 0);
|
|
296
|
-
export class ImageToVideoNode extends BaseNode {
|
|
297
|
-
static nodeType = "nodetool.video.ImageToVideo";
|
|
298
|
-
static title = "Image To Video";
|
|
299
|
-
static description = "Generate videos from input images using any supported video provider.\n Animates static images into dynamic video content with AI-powered motion.\n video, image-to-video, i2v, animation, AI, generation, sora, veo";
|
|
300
|
-
static metadataOutputTypes = {
|
|
301
|
-
output: "video"
|
|
302
|
-
};
|
|
303
|
-
static basicFields = [
|
|
304
|
-
"image",
|
|
305
|
-
"model",
|
|
306
|
-
"prompt",
|
|
307
|
-
"aspect_ratio",
|
|
308
|
-
"resolution",
|
|
309
|
-
"duration"
|
|
310
|
-
];
|
|
311
|
-
static exposeAsTool = true;
|
|
312
|
-
async process(context) {
|
|
313
|
-
const img = await imageBytesAsync(this.image, context);
|
|
314
|
-
const prompt = String(this.prompt ?? "");
|
|
315
|
-
const { providerId, modelId } = modelConfig(this.serialize());
|
|
316
|
-
if (canUseProvider(context, providerId, modelId)) {
|
|
317
|
-
const output = (await context.runProviderPrediction({
|
|
318
|
-
provider: providerId,
|
|
319
|
-
capability: "image_to_video",
|
|
320
|
-
model: modelId,
|
|
321
|
-
params: {
|
|
322
|
-
image: img,
|
|
323
|
-
prompt,
|
|
324
|
-
negative_prompt: this.negative_prompt,
|
|
325
|
-
duration_seconds: Number(this.duration ?? 0) || undefined,
|
|
326
|
-
aspect_ratio: this.aspect_ratio,
|
|
327
|
-
resolution: this.resolution
|
|
328
|
-
}
|
|
329
|
-
}));
|
|
330
|
-
return { output: videoRef(output) };
|
|
331
|
-
}
|
|
332
|
-
return {
|
|
333
|
-
output: videoRef(bytesConcat([img, Uint8Array.from(Buffer.from(prompt))]))
|
|
334
|
-
};
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
__decorate([
|
|
338
|
-
prop({
|
|
339
|
-
type: "image",
|
|
340
|
-
default: {
|
|
341
|
-
type: "image",
|
|
342
|
-
uri: "",
|
|
343
|
-
asset_id: null,
|
|
344
|
-
data: null,
|
|
345
|
-
metadata: null
|
|
346
|
-
},
|
|
347
|
-
title: "Image",
|
|
348
|
-
description: "The input image to animate into a video"
|
|
349
|
-
})
|
|
350
|
-
], ImageToVideoNode.prototype, "image", void 0);
|
|
351
|
-
__decorate([
|
|
352
|
-
prop({
|
|
353
|
-
type: "video_model",
|
|
354
|
-
default: {
|
|
355
|
-
type: "video_model",
|
|
356
|
-
provider: "gemini",
|
|
357
|
-
id: "veo-3.1-generate-preview",
|
|
358
|
-
name: "Veo 3.1 Preview",
|
|
359
|
-
path: null,
|
|
360
|
-
supported_tasks: []
|
|
361
|
-
},
|
|
362
|
-
title: "Model",
|
|
363
|
-
description: "The video generation model to use"
|
|
364
|
-
})
|
|
365
|
-
], ImageToVideoNode.prototype, "model", void 0);
|
|
366
|
-
__decorate([
|
|
367
|
-
prop({
|
|
368
|
-
type: "str",
|
|
369
|
-
default: "",
|
|
370
|
-
title: "Prompt",
|
|
371
|
-
description: "Optional text prompt to guide the video animation"
|
|
372
|
-
})
|
|
373
|
-
], ImageToVideoNode.prototype, "prompt", void 0);
|
|
374
|
-
__decorate([
|
|
375
|
-
prop({
|
|
376
|
-
type: "str",
|
|
377
|
-
default: "",
|
|
378
|
-
title: "Negative Prompt",
|
|
379
|
-
description: "Text prompt describing what to avoid in the video"
|
|
380
|
-
})
|
|
381
|
-
], ImageToVideoNode.prototype, "negative_prompt", void 0);
|
|
382
|
-
__decorate([
|
|
383
|
-
prop({
|
|
384
|
-
type: "str",
|
|
385
|
-
default: "16:9",
|
|
386
|
-
title: "Aspect Ratio",
|
|
387
|
-
description: "Aspect ratio for the video",
|
|
388
|
-
values: VIDEO_ASPECT_RATIO_VALUES,
|
|
389
|
-
json_schema_extra: { type: "media_aspect_ratio_video" }
|
|
390
|
-
})
|
|
391
|
-
], ImageToVideoNode.prototype, "aspect_ratio", void 0);
|
|
392
|
-
__decorate([
|
|
393
|
-
prop({
|
|
394
|
-
type: "str",
|
|
395
|
-
default: "1080p",
|
|
396
|
-
title: "Resolution",
|
|
397
|
-
description: "Video resolution",
|
|
398
|
-
values: VIDEO_RESOLUTION_VALUES,
|
|
399
|
-
json_schema_extra: { type: "media_resolution_video" }
|
|
400
|
-
})
|
|
401
|
-
], ImageToVideoNode.prototype, "resolution", void 0);
|
|
402
|
-
__decorate([
|
|
403
|
-
prop({
|
|
404
|
-
type: "int",
|
|
405
|
-
default: 4,
|
|
406
|
-
title: "Duration",
|
|
407
|
-
description: "Video duration in seconds",
|
|
408
|
-
values: VIDEO_DURATION_VALUES,
|
|
409
|
-
json_schema_extra: { type: "media_duration" }
|
|
410
|
-
})
|
|
411
|
-
], ImageToVideoNode.prototype, "duration", void 0);
|
|
412
|
-
__decorate([
|
|
413
|
-
prop({
|
|
414
|
-
type: "int",
|
|
415
|
-
default: 0,
|
|
416
|
-
title: "Timeout Seconds",
|
|
417
|
-
description: "Timeout in seconds for API calls (0 = use provider default)",
|
|
418
|
-
min: 0,
|
|
419
|
-
max: 7200
|
|
420
|
-
})
|
|
421
|
-
], ImageToVideoNode.prototype, "timeout_seconds", void 0);
|
|
422
|
-
export class LoadVideoFileNode extends BaseNode {
|
|
423
|
-
static nodeType = "nodetool.video.LoadVideoFile";
|
|
424
|
-
static title = "Load Video File";
|
|
425
|
-
static description = "Read a video file from disk.\n video, input, load, file";
|
|
426
|
-
static metadataOutputTypes = {
|
|
427
|
-
output: "video"
|
|
428
|
-
};
|
|
429
|
-
async process() {
|
|
430
|
-
const p = filePath(String(this.path ?? ""));
|
|
431
|
-
const data = new Uint8Array(await fs.readFile(p));
|
|
432
|
-
return { output: videoRef(data, { uri: `file://${p}` }) };
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
__decorate([
|
|
436
|
-
prop({
|
|
437
|
-
type: "str",
|
|
438
|
-
default: "",
|
|
439
|
-
title: "Path",
|
|
440
|
-
description: "Path to the video file to read"
|
|
441
|
-
})
|
|
442
|
-
], LoadVideoFileNode.prototype, "path", void 0);
|
|
443
|
-
export class SaveVideoFileVideoNode extends BaseNode {
|
|
444
|
-
static nodeType = "nodetool.video.SaveVideoFile";
|
|
445
|
-
static title = "Save Video File";
|
|
446
|
-
static description = "Write a video file to disk.\n video, output, save, file\n\n The filename can include time and date variables:\n %Y - Year, %m - Month, %d - Day\n %H - Hour, %M - Minute, %S - Second";
|
|
447
|
-
static metadataOutputTypes = {
|
|
448
|
-
output: "video"
|
|
449
|
-
};
|
|
450
|
-
async process(context) {
|
|
451
|
-
const folder = String(this.folder ?? ".");
|
|
452
|
-
const fname = dateName(String(this.filename ?? "video.mp4"));
|
|
453
|
-
const p = path.resolve(folder, fname);
|
|
454
|
-
await fs.mkdir(path.dirname(p), { recursive: true });
|
|
455
|
-
await fs.writeFile(p, await videoBytesAsync(this.video, context));
|
|
456
|
-
return { output: p };
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
__decorate([
|
|
460
|
-
prop({
|
|
461
|
-
type: "video",
|
|
462
|
-
default: {
|
|
463
|
-
type: "video",
|
|
464
|
-
uri: "",
|
|
465
|
-
asset_id: null,
|
|
466
|
-
data: null,
|
|
467
|
-
metadata: null,
|
|
468
|
-
duration: null,
|
|
469
|
-
format: null
|
|
470
|
-
},
|
|
471
|
-
title: "Video",
|
|
472
|
-
description: "The video to save"
|
|
473
|
-
})
|
|
474
|
-
], SaveVideoFileVideoNode.prototype, "video", void 0);
|
|
475
|
-
__decorate([
|
|
476
|
-
prop({
|
|
477
|
-
type: "str",
|
|
478
|
-
default: "",
|
|
479
|
-
title: "Folder",
|
|
480
|
-
description: "Folder where the file will be saved"
|
|
481
|
-
})
|
|
482
|
-
], SaveVideoFileVideoNode.prototype, "folder", void 0);
|
|
483
|
-
__decorate([
|
|
484
|
-
prop({
|
|
485
|
-
type: "str",
|
|
486
|
-
default: "",
|
|
487
|
-
title: "Filename",
|
|
488
|
-
description: "\n Name of the file to save.\n You can use time and date variables to create unique names:\n %Y - Year\n %m - Month\n %d - Day\n %H - Hour\n %M - Minute\n %S - Second\n "
|
|
489
|
-
})
|
|
490
|
-
], SaveVideoFileVideoNode.prototype, "filename", void 0);
|
|
491
|
-
export class LoadVideoAssetsNode extends BaseNode {
|
|
492
|
-
static nodeType = "nodetool.video.LoadVideoAssets";
|
|
493
|
-
static title = "Load Video Folder";
|
|
494
|
-
static description = "Load video files from an asset folder.\n\n video, assets, load";
|
|
495
|
-
static metadataOutputTypes = {
|
|
496
|
-
video: "video",
|
|
497
|
-
name: "str",
|
|
498
|
-
videos: "list",
|
|
499
|
-
names: "list"
|
|
500
|
-
};
|
|
501
|
-
static isStreamingOutput = true;
|
|
502
|
-
async process() {
|
|
503
|
-
const allVideos = [];
|
|
504
|
-
const allNames = [];
|
|
505
|
-
for await (const item of this._loadVideos()) {
|
|
506
|
-
allVideos.push(item.video);
|
|
507
|
-
allNames.push(item.name);
|
|
508
|
-
}
|
|
509
|
-
return {
|
|
510
|
-
video: allVideos[0] ?? null,
|
|
511
|
-
name: allNames[0] ?? "",
|
|
512
|
-
videos: allVideos,
|
|
513
|
-
names: allNames
|
|
514
|
-
};
|
|
515
|
-
}
|
|
516
|
-
async *_loadVideos() {
|
|
517
|
-
const raw = this.folder;
|
|
518
|
-
const folder = typeof raw === "string" && raw.length > 0
|
|
519
|
-
? raw.startsWith("file:")
|
|
520
|
-
? filePath(raw)
|
|
521
|
-
: raw
|
|
522
|
-
: typeof raw === "object" && raw !== null && typeof raw.uri === "string" && raw.uri.length > 0
|
|
523
|
-
? filePath(raw.uri)
|
|
524
|
-
: "";
|
|
525
|
-
if (!folder)
|
|
526
|
-
return;
|
|
527
|
-
let entries;
|
|
528
|
-
try {
|
|
529
|
-
entries = await fs.readdir(folder, { withFileTypes: true });
|
|
530
|
-
}
|
|
531
|
-
catch {
|
|
532
|
-
// folder does not exist or is not accessible
|
|
533
|
-
return;
|
|
534
|
-
}
|
|
535
|
-
for (const entry of entries) {
|
|
536
|
-
if (!entry.isFile())
|
|
537
|
-
continue;
|
|
538
|
-
const ext = path.extname(entry.name).toLowerCase();
|
|
539
|
-
if (![".mp4", ".mov", ".webm", ".mkv", ".avi"].includes(ext))
|
|
540
|
-
continue;
|
|
541
|
-
const full = path.join(folder, entry.name);
|
|
542
|
-
const data = new Uint8Array(await fs.readFile(full));
|
|
543
|
-
yield {
|
|
544
|
-
video: videoRef(data, { uri: `file://${full}` }),
|
|
545
|
-
name: entry.name
|
|
546
|
-
};
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
async *genProcess() {
|
|
550
|
-
const allVideos = [];
|
|
551
|
-
const allNames = [];
|
|
552
|
-
for await (const item of this._loadVideos()) {
|
|
553
|
-
allVideos.push(item.video);
|
|
554
|
-
allNames.push(item.name);
|
|
555
|
-
yield { video: item.video, name: item.name };
|
|
556
|
-
}
|
|
557
|
-
// Emit collected lists as final output
|
|
558
|
-
yield { videos: allVideos, names: allNames };
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
__decorate([
|
|
562
|
-
prop({
|
|
563
|
-
type: "folder",
|
|
564
|
-
default: {
|
|
565
|
-
type: "folder",
|
|
566
|
-
uri: "",
|
|
567
|
-
asset_id: null,
|
|
568
|
-
data: null,
|
|
569
|
-
metadata: null
|
|
570
|
-
},
|
|
571
|
-
title: "Folder",
|
|
572
|
-
description: "The asset folder to load the video files from."
|
|
573
|
-
})
|
|
574
|
-
], LoadVideoAssetsNode.prototype, "folder", void 0);
|
|
575
|
-
export class SaveVideoNode extends BaseNode {
|
|
576
|
-
static nodeType = "nodetool.video.SaveVideo";
|
|
577
|
-
static title = "Save Video Asset";
|
|
578
|
-
static description = "Save a video to an asset folder.\n video, save, file, output";
|
|
579
|
-
static metadataOutputTypes = {
|
|
580
|
-
output: "video"
|
|
581
|
-
};
|
|
582
|
-
async process(context) {
|
|
583
|
-
const folder = String(this.folder ?? ".");
|
|
584
|
-
const filename = dateName(String(this.name ?? "video.mp4"));
|
|
585
|
-
const full = path.resolve(folder, filename);
|
|
586
|
-
await fs.mkdir(path.dirname(full), { recursive: true });
|
|
587
|
-
const bytes = await videoBytesAsync(this.video, context);
|
|
588
|
-
await fs.writeFile(full, bytes);
|
|
589
|
-
return { output: videoRef(bytes, { uri: `file://${full}` }) };
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
__decorate([
|
|
593
|
-
prop({
|
|
594
|
-
type: "video",
|
|
595
|
-
default: {
|
|
596
|
-
type: "video",
|
|
597
|
-
uri: "",
|
|
598
|
-
asset_id: null,
|
|
599
|
-
data: null,
|
|
600
|
-
metadata: null,
|
|
601
|
-
duration: null,
|
|
602
|
-
format: null
|
|
603
|
-
},
|
|
604
|
-
title: "Video",
|
|
605
|
-
description: "The video to save."
|
|
606
|
-
})
|
|
607
|
-
], SaveVideoNode.prototype, "video", void 0);
|
|
608
|
-
__decorate([
|
|
609
|
-
prop({
|
|
610
|
-
type: "folder",
|
|
611
|
-
default: {
|
|
612
|
-
type: "folder",
|
|
613
|
-
uri: "",
|
|
614
|
-
asset_id: null,
|
|
615
|
-
data: null,
|
|
616
|
-
metadata: null
|
|
617
|
-
},
|
|
618
|
-
title: "Folder",
|
|
619
|
-
description: "The asset folder to save the video in."
|
|
620
|
-
})
|
|
621
|
-
], SaveVideoNode.prototype, "folder", void 0);
|
|
622
|
-
__decorate([
|
|
623
|
-
prop({
|
|
624
|
-
type: "str",
|
|
625
|
-
default: "%Y-%m-%d-%H-%M-%S.mp4",
|
|
626
|
-
title: "Name",
|
|
627
|
-
description: "\n Name of the output video.\n You can use time and date variables to create unique names:\n %Y - Year\n %m - Month\n %d - Day\n %H - Hour\n %M - Minute\n %S - Second\n "
|
|
628
|
-
})
|
|
629
|
-
], SaveVideoNode.prototype, "name", void 0);
|
|
630
|
-
export class FrameIteratorNode extends BaseNode {
|
|
631
|
-
static nodeType = "nodetool.video.FrameIterator";
|
|
632
|
-
static title = "Frame Iterator";
|
|
633
|
-
static description = "Extract frames from a video file using OpenCV.\n video, frames, extract, sequence";
|
|
634
|
-
static metadataOutputTypes = {
|
|
635
|
-
frame: "image",
|
|
636
|
-
index: "int",
|
|
637
|
-
fps: "float"
|
|
638
|
-
};
|
|
639
|
-
static isStreamingOutput = true;
|
|
640
|
-
async process() {
|
|
641
|
-
return {};
|
|
642
|
-
}
|
|
643
|
-
async *genProcess(context) {
|
|
644
|
-
const bytes = await videoBytesAsync(this.video, context);
|
|
645
|
-
if (bytes.length === 0)
|
|
646
|
-
return;
|
|
647
|
-
const inputFile = await withTempFile(".mp4", bytes);
|
|
648
|
-
const outputDir = await fs.mkdtemp(path.join(os.tmpdir(), "nodetool-frames-out-"));
|
|
649
|
-
try {
|
|
650
|
-
// Get FPS from the video for output metadata
|
|
651
|
-
let fps = 24;
|
|
652
|
-
try {
|
|
653
|
-
const { stdout } = await execFile("ffprobe", [
|
|
654
|
-
"-v", "error",
|
|
655
|
-
"-select_streams", "v:0",
|
|
656
|
-
"-show_entries", "stream=r_frame_rate",
|
|
657
|
-
"-of", "csv=p=0",
|
|
658
|
-
inputFile.path
|
|
659
|
-
]);
|
|
660
|
-
const parts = stdout.trim().split("/");
|
|
661
|
-
if (parts.length === 2) {
|
|
662
|
-
fps = Number(parts[0]) / Number(parts[1]);
|
|
663
|
-
}
|
|
664
|
-
else if (parts.length === 1 && Number(parts[0]) > 0) {
|
|
665
|
-
fps = Number(parts[0]);
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
catch {
|
|
669
|
-
// fall back to 24 fps
|
|
670
|
-
}
|
|
671
|
-
const start = Math.max(0, Number(this.start ?? 0));
|
|
672
|
-
const end = Number(this.end ?? -1);
|
|
673
|
-
const vfFilter = end >= 0
|
|
674
|
-
? `select='between(n,${start},${end})'`
|
|
675
|
-
: `select='gte(n,${start})'`;
|
|
676
|
-
await execFile("ffmpeg", [
|
|
677
|
-
"-y",
|
|
678
|
-
"-i", inputFile.path,
|
|
679
|
-
"-vf", vfFilter,
|
|
680
|
-
"-vsync", "vfr",
|
|
681
|
-
path.join(outputDir, "frame_%d.png")
|
|
682
|
-
], { maxBuffer: 50 * 1024 * 1024 });
|
|
683
|
-
// Read extracted frames in order
|
|
684
|
-
const entries = await fs.readdir(outputDir);
|
|
685
|
-
const frameFiles = entries
|
|
686
|
-
.filter((f) => f.startsWith("frame_") && f.endsWith(".png"))
|
|
687
|
-
.sort((a, b) => {
|
|
688
|
-
const numA = parseInt(a.replace("frame_", "").replace(".png", ""), 10);
|
|
689
|
-
const numB = parseInt(b.replace("frame_", "").replace(".png", ""), 10);
|
|
690
|
-
return numA - numB;
|
|
691
|
-
});
|
|
692
|
-
for (let i = 0; i < frameFiles.length; i++) {
|
|
693
|
-
const frameData = new Uint8Array(await fs.readFile(path.join(outputDir, frameFiles[i])));
|
|
694
|
-
yield {
|
|
695
|
-
frame: {
|
|
696
|
-
type: "image",
|
|
697
|
-
data: Buffer.from(frameData).toString("base64")
|
|
698
|
-
},
|
|
699
|
-
index: start + i,
|
|
700
|
-
fps
|
|
701
|
-
};
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
finally {
|
|
705
|
-
await inputFile.cleanup();
|
|
706
|
-
await fs.rm(outputDir, { recursive: true, force: true });
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
__decorate([
|
|
711
|
-
prop({
|
|
712
|
-
type: "video",
|
|
713
|
-
default: {
|
|
714
|
-
type: "video",
|
|
715
|
-
uri: "",
|
|
716
|
-
asset_id: null,
|
|
717
|
-
data: null,
|
|
718
|
-
metadata: null,
|
|
719
|
-
duration: null,
|
|
720
|
-
format: null
|
|
721
|
-
},
|
|
722
|
-
title: "Video",
|
|
723
|
-
description: "The input video to extract frames from."
|
|
724
|
-
})
|
|
725
|
-
], FrameIteratorNode.prototype, "video", void 0);
|
|
726
|
-
__decorate([
|
|
727
|
-
prop({
|
|
728
|
-
type: "int",
|
|
729
|
-
default: 0,
|
|
730
|
-
title: "Start",
|
|
731
|
-
description: "The frame to start extracting from."
|
|
732
|
-
})
|
|
733
|
-
], FrameIteratorNode.prototype, "start", void 0);
|
|
734
|
-
__decorate([
|
|
735
|
-
prop({
|
|
736
|
-
type: "int",
|
|
737
|
-
default: -1,
|
|
738
|
-
title: "End",
|
|
739
|
-
description: "The frame to stop extracting from."
|
|
740
|
-
})
|
|
741
|
-
], FrameIteratorNode.prototype, "end", void 0);
|
|
742
|
-
export class FpsNode extends BaseNode {
|
|
743
|
-
static nodeType = "nodetool.video.Fps";
|
|
744
|
-
static title = "Fps";
|
|
745
|
-
static description = "Get the frames per second (FPS) of a video file.\n video, analysis, frames, fps";
|
|
746
|
-
static metadataOutputTypes = {
|
|
747
|
-
output: "float"
|
|
748
|
-
};
|
|
749
|
-
async process(context) {
|
|
750
|
-
const bytes = await videoBytesAsync(this.video, context);
|
|
751
|
-
if (bytes.length === 0)
|
|
752
|
-
return { output: 0 };
|
|
753
|
-
const inputFile = await withTempFile(".mp4", bytes);
|
|
754
|
-
try {
|
|
755
|
-
const { stdout } = await execFile("ffprobe", [
|
|
756
|
-
"-v", "error",
|
|
757
|
-
"-select_streams", "v:0",
|
|
758
|
-
"-show_entries", "stream=r_frame_rate",
|
|
759
|
-
"-of", "csv=p=0",
|
|
760
|
-
inputFile.path
|
|
761
|
-
]);
|
|
762
|
-
const parts = stdout.trim().split("/");
|
|
763
|
-
let fps = 0;
|
|
764
|
-
if (parts.length === 2) {
|
|
765
|
-
fps = Number(parts[0]) / Number(parts[1]);
|
|
766
|
-
}
|
|
767
|
-
else if (parts.length === 1 && Number(parts[0]) > 0) {
|
|
768
|
-
fps = Number(parts[0]);
|
|
769
|
-
}
|
|
770
|
-
return { output: fps };
|
|
771
|
-
}
|
|
772
|
-
catch {
|
|
773
|
-
return { output: 0 };
|
|
774
|
-
}
|
|
775
|
-
finally {
|
|
776
|
-
await inputFile.cleanup();
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
__decorate([
|
|
781
|
-
prop({
|
|
782
|
-
type: "video",
|
|
783
|
-
default: {
|
|
784
|
-
type: "video",
|
|
785
|
-
uri: "",
|
|
786
|
-
asset_id: null,
|
|
787
|
-
data: null,
|
|
788
|
-
metadata: null,
|
|
789
|
-
duration: null,
|
|
790
|
-
format: null
|
|
791
|
-
},
|
|
792
|
-
title: "Video",
|
|
793
|
-
description: "The input video to analyze for FPS."
|
|
794
|
-
})
|
|
795
|
-
], FpsNode.prototype, "video", void 0);
|
|
796
|
-
export class FrameToVideoNode extends BaseNode {
|
|
797
|
-
static nodeType = "nodetool.video.FrameToVideo";
|
|
798
|
-
static title = "Frame To Video";
|
|
799
|
-
static description = "Combine a sequence of frames into a single video file.\n video, frames, combine, sequence";
|
|
800
|
-
static isStreamingInput = true;
|
|
801
|
-
static metadataOutputTypes = {
|
|
802
|
-
output: "video"
|
|
803
|
-
};
|
|
804
|
-
async run(inputs, outputs, _context) {
|
|
805
|
-
// Collect all frames from the streaming input
|
|
806
|
-
const frames = [];
|
|
807
|
-
let fps = Math.max(1, Number(this.fps ?? 30));
|
|
808
|
-
for await (const [handle, item] of inputs.any()) {
|
|
809
|
-
if (handle === "frame") {
|
|
810
|
-
frames.push(item);
|
|
811
|
-
}
|
|
812
|
-
else if (handle === "fps") {
|
|
813
|
-
fps = Math.max(1, Number(item ?? 30));
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
if (frames.length === 0) {
|
|
817
|
-
await outputs.emit("output", videoRef(new Uint8Array()));
|
|
818
|
-
return;
|
|
819
|
-
}
|
|
820
|
-
const inputDir = await fs.mkdtemp(path.join(os.tmpdir(), "nodetool-ftv-in-"));
|
|
821
|
-
const outputDir = await fs.mkdtemp(path.join(os.tmpdir(), "nodetool-ftv-out-"));
|
|
822
|
-
const outputPath = path.join(outputDir, "output.mp4");
|
|
823
|
-
try {
|
|
824
|
-
// Write each frame as a numbered PNG file
|
|
825
|
-
for (let i = 0; i < frames.length; i++) {
|
|
826
|
-
const f = frames[i];
|
|
827
|
-
if (!f || typeof f !== "object")
|
|
828
|
-
continue;
|
|
829
|
-
const frameBytes = toBytes(f.data);
|
|
830
|
-
if (frameBytes.length === 0)
|
|
831
|
-
continue;
|
|
832
|
-
await fs.writeFile(path.join(inputDir, `frame_${String(i + 1).padStart(6, "0")}.png`), frameBytes);
|
|
833
|
-
}
|
|
834
|
-
await execFile("ffmpeg", [
|
|
835
|
-
"-y",
|
|
836
|
-
"-framerate", String(fps),
|
|
837
|
-
"-i", path.join(inputDir, "frame_%06d.png"),
|
|
838
|
-
"-c:v", "libx264",
|
|
839
|
-
"-pix_fmt", "yuv420p",
|
|
840
|
-
outputPath
|
|
841
|
-
], { maxBuffer: 50 * 1024 * 1024 });
|
|
842
|
-
const result = new Uint8Array(await fs.readFile(outputPath));
|
|
843
|
-
await outputs.emit("output", videoRef(result));
|
|
844
|
-
}
|
|
845
|
-
catch {
|
|
846
|
-
// Fallback: concatenate raw frame bytes
|
|
847
|
-
const parts = frames.map((f) => {
|
|
848
|
-
if (!f || typeof f !== "object")
|
|
849
|
-
return new Uint8Array();
|
|
850
|
-
return toBytes(f.data);
|
|
851
|
-
});
|
|
852
|
-
await outputs.emit("output", videoRef(bytesConcat(parts)));
|
|
853
|
-
}
|
|
854
|
-
finally {
|
|
855
|
-
await fs.rm(inputDir, { recursive: true, force: true });
|
|
856
|
-
await fs.rm(outputDir, { recursive: true, force: true });
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
async process() {
|
|
860
|
-
// Legacy fallback for non-streaming usage (e.g., direct array input)
|
|
861
|
-
const frames = Array.isArray(this.frame) ? this.frame : [];
|
|
862
|
-
if (frames.length === 0)
|
|
863
|
-
return { output: videoRef(new Uint8Array()) };
|
|
864
|
-
const inputDir = await fs.mkdtemp(path.join(os.tmpdir(), "nodetool-ftv-in-"));
|
|
865
|
-
const outputDir = await fs.mkdtemp(path.join(os.tmpdir(), "nodetool-ftv-out-"));
|
|
866
|
-
const outputPath = path.join(outputDir, "output.mp4");
|
|
867
|
-
try {
|
|
868
|
-
for (let i = 0; i < frames.length; i++) {
|
|
869
|
-
const f = frames[i];
|
|
870
|
-
if (!f || typeof f !== "object")
|
|
871
|
-
continue;
|
|
872
|
-
const frameBytes = toBytes(f.data);
|
|
873
|
-
if (frameBytes.length === 0)
|
|
874
|
-
continue;
|
|
875
|
-
await fs.writeFile(path.join(inputDir, `frame_${String(i + 1).padStart(6, "0")}.png`), frameBytes);
|
|
876
|
-
}
|
|
877
|
-
const fps = Math.max(1, Number(this.fps ?? 30));
|
|
878
|
-
await execFile("ffmpeg", [
|
|
879
|
-
"-y",
|
|
880
|
-
"-framerate", String(fps),
|
|
881
|
-
"-i", path.join(inputDir, "frame_%06d.png"),
|
|
882
|
-
"-c:v", "libx264",
|
|
883
|
-
"-pix_fmt", "yuv420p",
|
|
884
|
-
outputPath
|
|
885
|
-
], { maxBuffer: 50 * 1024 * 1024 });
|
|
886
|
-
const result = new Uint8Array(await fs.readFile(outputPath));
|
|
887
|
-
return { output: videoRef(result) };
|
|
888
|
-
}
|
|
889
|
-
catch {
|
|
890
|
-
const parts = frames.map((f) => {
|
|
891
|
-
if (!f || typeof f !== "object")
|
|
892
|
-
return new Uint8Array();
|
|
893
|
-
return toBytes(f.data);
|
|
894
|
-
});
|
|
895
|
-
return { output: videoRef(bytesConcat(parts)) };
|
|
896
|
-
}
|
|
897
|
-
finally {
|
|
898
|
-
await fs.rm(inputDir, { recursive: true, force: true });
|
|
899
|
-
await fs.rm(outputDir, { recursive: true, force: true });
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
__decorate([
|
|
904
|
-
prop({
|
|
905
|
-
type: "image",
|
|
906
|
-
default: {
|
|
907
|
-
type: "image",
|
|
908
|
-
uri: "",
|
|
909
|
-
asset_id: null,
|
|
910
|
-
data: null,
|
|
911
|
-
metadata: null
|
|
912
|
-
},
|
|
913
|
-
title: "Frame",
|
|
914
|
-
description: "Collect input frames"
|
|
915
|
-
})
|
|
916
|
-
], FrameToVideoNode.prototype, "frame", void 0);
|
|
917
|
-
__decorate([
|
|
918
|
-
prop({
|
|
919
|
-
type: "float",
|
|
920
|
-
default: 30,
|
|
921
|
-
title: "Fps",
|
|
922
|
-
description: "The FPS of the output video."
|
|
923
|
-
})
|
|
924
|
-
], FrameToVideoNode.prototype, "fps", void 0);
|
|
925
|
-
class VideoTransformNode extends BaseNode {
|
|
926
|
-
static requiredRuntimes = ["ffmpeg"];
|
|
927
|
-
async process(context) {
|
|
928
|
-
const bytes = await videoBytesAsync(this.video, context);
|
|
929
|
-
return { output: videoRef(bytes) };
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
export class ConcatVideoNode extends BaseNode {
|
|
933
|
-
static nodeType = "nodetool.video.Concat";
|
|
934
|
-
static title = "Concat";
|
|
935
|
-
static description = "Concatenate multiple video files into a single video, including audio when available.\n video, concat, merge, combine, audio, +";
|
|
936
|
-
static requiredRuntimes = ["ffmpeg"];
|
|
937
|
-
static metadataOutputTypes = {
|
|
938
|
-
output: "video"
|
|
939
|
-
};
|
|
940
|
-
static isDynamic = true;
|
|
941
|
-
async process(context) {
|
|
942
|
-
const inputValues = [
|
|
943
|
-
this.video_a,
|
|
944
|
-
this.video_b,
|
|
945
|
-
...Array.from(this.dynamicProps.values())
|
|
946
|
-
];
|
|
947
|
-
const parts = [];
|
|
948
|
-
for (const input of inputValues) {
|
|
949
|
-
const bytes = await videoBytesAsync(input, context);
|
|
950
|
-
if (bytes.length > 0) {
|
|
951
|
-
parts.push(bytes);
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
if (parts.length === 0)
|
|
955
|
-
return { output: videoRef(new Uint8Array()) };
|
|
956
|
-
if (parts.length === 1)
|
|
957
|
-
return { output: videoRef(parts[0]) };
|
|
958
|
-
const inputs = await Promise.all(parts.map((bytes) => withTempFile(".mp4", bytes)));
|
|
959
|
-
const concatDir = await fs.mkdtemp(path.join(os.tmpdir(), "nodetool-concat-"));
|
|
960
|
-
const listPath = path.join(concatDir, "list.txt");
|
|
961
|
-
const outputPath = path.join(concatDir, "output.mp4");
|
|
962
|
-
try {
|
|
963
|
-
const listEntries = inputs
|
|
964
|
-
.map((input) => `file '${input.path}'`)
|
|
965
|
-
.join("\n");
|
|
966
|
-
await fs.writeFile(listPath, `${listEntries}\n`);
|
|
967
|
-
await execFile("ffmpeg", ["-y", "-f", "concat", "-safe", "0", "-i", listPath, "-c", "copy", outputPath], { maxBuffer: 50 * 1024 * 1024 });
|
|
968
|
-
const result = new Uint8Array(await fs.readFile(outputPath));
|
|
969
|
-
return { output: videoRef(result) };
|
|
970
|
-
}
|
|
971
|
-
catch {
|
|
972
|
-
return { output: videoRef(bytesConcat(parts)) };
|
|
973
|
-
}
|
|
974
|
-
finally {
|
|
975
|
-
for (const input of inputs) {
|
|
976
|
-
await input.cleanup();
|
|
977
|
-
}
|
|
978
|
-
await fs.rm(concatDir, { recursive: true, force: true });
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
__decorate([
|
|
983
|
-
prop({
|
|
984
|
-
type: "video",
|
|
985
|
-
default: {
|
|
986
|
-
type: "video",
|
|
987
|
-
uri: "",
|
|
988
|
-
asset_id: null,
|
|
989
|
-
data: null,
|
|
990
|
-
metadata: null,
|
|
991
|
-
duration: null,
|
|
992
|
-
format: null
|
|
993
|
-
},
|
|
994
|
-
title: "Video A",
|
|
995
|
-
description: "The first video to concatenate."
|
|
996
|
-
})
|
|
997
|
-
], ConcatVideoNode.prototype, "video_a", void 0);
|
|
998
|
-
__decorate([
|
|
999
|
-
prop({
|
|
1000
|
-
type: "video",
|
|
1001
|
-
default: {
|
|
1002
|
-
type: "video",
|
|
1003
|
-
uri: "",
|
|
1004
|
-
asset_id: null,
|
|
1005
|
-
data: null,
|
|
1006
|
-
metadata: null,
|
|
1007
|
-
duration: null,
|
|
1008
|
-
format: null
|
|
1009
|
-
},
|
|
1010
|
-
title: "Video B",
|
|
1011
|
-
description: "The second video to concatenate."
|
|
1012
|
-
})
|
|
1013
|
-
], ConcatVideoNode.prototype, "video_b", void 0);
|
|
1014
|
-
export class TrimVideoNode extends BaseNode {
|
|
1015
|
-
static nodeType = "nodetool.video.Trim";
|
|
1016
|
-
static title = "Trim";
|
|
1017
|
-
static description = "Trim a video to a specific start and end time.\n video, trim, cut, segment";
|
|
1018
|
-
static requiredRuntimes = ["ffmpeg"];
|
|
1019
|
-
static metadataOutputTypes = {
|
|
1020
|
-
output: "video"
|
|
1021
|
-
};
|
|
1022
|
-
async process(context) {
|
|
1023
|
-
const bytes = await videoBytesAsync(this.video, context);
|
|
1024
|
-
if (bytes.length === 0)
|
|
1025
|
-
return { output: videoRef(bytes) };
|
|
1026
|
-
const start = Math.max(0, Number(this.start_time ?? 0));
|
|
1027
|
-
const end = Number(this.end_time ?? -1);
|
|
1028
|
-
const args = ["-ss", String(start)];
|
|
1029
|
-
if (end >= 0) {
|
|
1030
|
-
args.push("-to", String(end));
|
|
1031
|
-
}
|
|
1032
|
-
args.push("-c", "copy");
|
|
1033
|
-
const transformed = (await ffmpegTransform(bytes, args)) ?? bytes;
|
|
1034
|
-
return { output: videoRef(transformed) };
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
__decorate([
|
|
1038
|
-
prop({
|
|
1039
|
-
type: "video",
|
|
1040
|
-
default: {
|
|
1041
|
-
type: "video",
|
|
1042
|
-
uri: "",
|
|
1043
|
-
asset_id: null,
|
|
1044
|
-
data: null,
|
|
1045
|
-
metadata: null,
|
|
1046
|
-
duration: null,
|
|
1047
|
-
format: null
|
|
1048
|
-
},
|
|
1049
|
-
title: "Video",
|
|
1050
|
-
description: "The input video to trim."
|
|
1051
|
-
})
|
|
1052
|
-
], TrimVideoNode.prototype, "video", void 0);
|
|
1053
|
-
__decorate([
|
|
1054
|
-
prop({
|
|
1055
|
-
type: "float",
|
|
1056
|
-
default: 0,
|
|
1057
|
-
title: "Start Time",
|
|
1058
|
-
description: "The start time in seconds for the trimmed video."
|
|
1059
|
-
})
|
|
1060
|
-
], TrimVideoNode.prototype, "start_time", void 0);
|
|
1061
|
-
__decorate([
|
|
1062
|
-
prop({
|
|
1063
|
-
type: "float",
|
|
1064
|
-
default: -1,
|
|
1065
|
-
title: "End Time",
|
|
1066
|
-
description: "The end time in seconds for the trimmed video. Use -1 for the end of the video."
|
|
1067
|
-
})
|
|
1068
|
-
], TrimVideoNode.prototype, "end_time", void 0);
|
|
1069
|
-
export class ResizeVideoNode extends VideoTransformNode {
|
|
1070
|
-
static nodeType = "nodetool.video.Resize";
|
|
1071
|
-
static title = "Resize";
|
|
1072
|
-
static description = "Resize a video to a specific width and height.\n video, resize, scale, dimensions";
|
|
1073
|
-
static metadataOutputTypes = {
|
|
1074
|
-
output: "video"
|
|
1075
|
-
};
|
|
1076
|
-
async process(context) {
|
|
1077
|
-
const bytes = await videoBytesAsync(this.video, context);
|
|
1078
|
-
const width = Number(this.width ?? -1);
|
|
1079
|
-
const height = Number(this.height ?? -1);
|
|
1080
|
-
const transformed = (await ffmpegTransform(bytes, [
|
|
1081
|
-
"-vf",
|
|
1082
|
-
`scale=${width}:${height}`,
|
|
1083
|
-
"-c:a",
|
|
1084
|
-
"copy"
|
|
1085
|
-
])) ?? bytes;
|
|
1086
|
-
return { output: videoRef(transformed) };
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1089
|
-
__decorate([
|
|
1090
|
-
prop({
|
|
1091
|
-
type: "video",
|
|
1092
|
-
default: {
|
|
1093
|
-
type: "video",
|
|
1094
|
-
uri: "",
|
|
1095
|
-
asset_id: null,
|
|
1096
|
-
data: null,
|
|
1097
|
-
metadata: null,
|
|
1098
|
-
duration: null,
|
|
1099
|
-
format: null
|
|
1100
|
-
},
|
|
1101
|
-
title: "Video",
|
|
1102
|
-
description: "The input video to resize."
|
|
1103
|
-
})
|
|
1104
|
-
], ResizeVideoNode.prototype, "video", void 0);
|
|
1105
|
-
__decorate([
|
|
1106
|
-
prop({
|
|
1107
|
-
type: "int",
|
|
1108
|
-
default: -1,
|
|
1109
|
-
title: "Width",
|
|
1110
|
-
description: "The target width. Use -1 to maintain aspect ratio."
|
|
1111
|
-
})
|
|
1112
|
-
], ResizeVideoNode.prototype, "width", void 0);
|
|
1113
|
-
__decorate([
|
|
1114
|
-
prop({
|
|
1115
|
-
type: "int",
|
|
1116
|
-
default: -1,
|
|
1117
|
-
title: "Height",
|
|
1118
|
-
description: "The target height. Use -1 to maintain aspect ratio."
|
|
1119
|
-
})
|
|
1120
|
-
], ResizeVideoNode.prototype, "height", void 0);
|
|
1121
|
-
export class RotateVideoNode extends VideoTransformNode {
|
|
1122
|
-
static nodeType = "nodetool.video.Rotate";
|
|
1123
|
-
static title = "Rotate";
|
|
1124
|
-
static description = "Rotate a video by a specified angle.\n video, rotate, orientation, transform";
|
|
1125
|
-
static metadataOutputTypes = {
|
|
1126
|
-
output: "video"
|
|
1127
|
-
};
|
|
1128
|
-
async process(context) {
|
|
1129
|
-
const bytes = await videoBytesAsync(this.video, context);
|
|
1130
|
-
const angle = Number(this.angle ?? 90);
|
|
1131
|
-
const radians = (angle * Math.PI) / 180;
|
|
1132
|
-
const transformed = (await ffmpegTransform(bytes, [
|
|
1133
|
-
"-vf",
|
|
1134
|
-
`rotate=${radians}:ow=rotw(${radians}):oh=roth(${radians})`,
|
|
1135
|
-
"-c:a",
|
|
1136
|
-
"copy"
|
|
1137
|
-
])) ?? bytes;
|
|
1138
|
-
return { output: videoRef(transformed) };
|
|
1139
|
-
}
|
|
1140
|
-
}
|
|
1141
|
-
__decorate([
|
|
1142
|
-
prop({
|
|
1143
|
-
type: "video",
|
|
1144
|
-
default: {
|
|
1145
|
-
type: "video",
|
|
1146
|
-
uri: "",
|
|
1147
|
-
asset_id: null,
|
|
1148
|
-
data: null,
|
|
1149
|
-
metadata: null,
|
|
1150
|
-
duration: null,
|
|
1151
|
-
format: null
|
|
1152
|
-
},
|
|
1153
|
-
title: "Video",
|
|
1154
|
-
description: "The input video to rotate."
|
|
1155
|
-
})
|
|
1156
|
-
], RotateVideoNode.prototype, "video", void 0);
|
|
1157
|
-
__decorate([
|
|
1158
|
-
prop({
|
|
1159
|
-
type: "float",
|
|
1160
|
-
default: 0,
|
|
1161
|
-
title: "Angle",
|
|
1162
|
-
description: "The angle of rotation in degrees.",
|
|
1163
|
-
min: -360,
|
|
1164
|
-
max: 360
|
|
1165
|
-
})
|
|
1166
|
-
], RotateVideoNode.prototype, "angle", void 0);
|
|
1167
|
-
export class SetSpeedVideoNode extends VideoTransformNode {
|
|
1168
|
-
static nodeType = "nodetool.video.SetSpeed";
|
|
1169
|
-
static title = "Set Speed";
|
|
1170
|
-
static description = "Adjust the playback speed of a video.\n video, speed, tempo, time";
|
|
1171
|
-
static metadataOutputTypes = {
|
|
1172
|
-
output: "video"
|
|
1173
|
-
};
|
|
1174
|
-
async process(context) {
|
|
1175
|
-
const bytes = await videoBytesAsync(this.video, context);
|
|
1176
|
-
const speed = Math.max(0.1, Number(this.speed_factor ?? 1));
|
|
1177
|
-
// Build chained atempo filters for values outside 0.5-2.0 range
|
|
1178
|
-
// Each atempo filter is limited to 0.5-2.0, so we chain them
|
|
1179
|
-
const buildAtempo = (s) => {
|
|
1180
|
-
const parts = [];
|
|
1181
|
-
let remaining = s;
|
|
1182
|
-
if (remaining > 1) {
|
|
1183
|
-
while (remaining > 2) {
|
|
1184
|
-
parts.push("atempo=2.0");
|
|
1185
|
-
remaining /= 2;
|
|
1186
|
-
}
|
|
1187
|
-
parts.push(`atempo=${remaining}`);
|
|
1188
|
-
}
|
|
1189
|
-
else {
|
|
1190
|
-
while (remaining < 0.5) {
|
|
1191
|
-
parts.push("atempo=0.5");
|
|
1192
|
-
remaining /= 0.5;
|
|
1193
|
-
}
|
|
1194
|
-
parts.push(`atempo=${remaining}`);
|
|
1195
|
-
}
|
|
1196
|
-
return parts.join(",");
|
|
1197
|
-
};
|
|
1198
|
-
const atempoChain = buildAtempo(speed);
|
|
1199
|
-
const transformed = (await ffmpegTransform(bytes, [
|
|
1200
|
-
"-filter_complex",
|
|
1201
|
-
`[0:v]setpts=${1 / speed}*PTS[v];[0:a]${atempoChain}[a]`,
|
|
1202
|
-
"-map",
|
|
1203
|
-
"[v]",
|
|
1204
|
-
"-map",
|
|
1205
|
-
"[a]"
|
|
1206
|
-
])) ??
|
|
1207
|
-
(await ffmpegTransform(bytes, ["-vf", `setpts=${1 / speed}*PTS`])) ??
|
|
1208
|
-
bytes;
|
|
1209
|
-
return { output: videoRef(transformed) };
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1212
|
-
__decorate([
|
|
1213
|
-
prop({
|
|
1214
|
-
type: "video",
|
|
1215
|
-
default: {
|
|
1216
|
-
type: "video",
|
|
1217
|
-
uri: "",
|
|
1218
|
-
asset_id: null,
|
|
1219
|
-
data: null,
|
|
1220
|
-
metadata: null,
|
|
1221
|
-
duration: null,
|
|
1222
|
-
format: null
|
|
1223
|
-
},
|
|
1224
|
-
title: "Video",
|
|
1225
|
-
description: "The input video to adjust speed."
|
|
1226
|
-
})
|
|
1227
|
-
], SetSpeedVideoNode.prototype, "video", void 0);
|
|
1228
|
-
__decorate([
|
|
1229
|
-
prop({
|
|
1230
|
-
type: "float",
|
|
1231
|
-
default: 1,
|
|
1232
|
-
title: "Speed Factor",
|
|
1233
|
-
description: "The speed adjustment factor. Values > 1 speed up, < 1 slow down."
|
|
1234
|
-
})
|
|
1235
|
-
], SetSpeedVideoNode.prototype, "speed_factor", void 0);
|
|
1236
|
-
export class OverlayVideoNode extends VideoTransformNode {
|
|
1237
|
-
static nodeType = "nodetool.video.Overlay";
|
|
1238
|
-
static title = "Overlay";
|
|
1239
|
-
static description = "Overlay one video on top of another, including audio overlay when available.\n video, overlay, composite, picture-in-picture, audio";
|
|
1240
|
-
static metadataOutputTypes = {
|
|
1241
|
-
output: "video"
|
|
1242
|
-
};
|
|
1243
|
-
async process(context) {
|
|
1244
|
-
const mainVideo = await videoBytesAsync(this.main_video, context);
|
|
1245
|
-
const overlayVideo = await videoBytesAsync(this.overlay_video, context);
|
|
1246
|
-
if (overlayVideo.length === 0)
|
|
1247
|
-
return { output: videoRef(mainVideo) };
|
|
1248
|
-
const x = Number(this.x ?? 0);
|
|
1249
|
-
const y = Number(this.y ?? 0);
|
|
1250
|
-
const scale = Math.max(0.01, Number(this.scale ?? 1));
|
|
1251
|
-
const vol = Math.max(0, Math.min(1, Number(this.overlay_audio_volume ?? 0.5)));
|
|
1252
|
-
// Try with audio mixing first, fall back to video-only overlay
|
|
1253
|
-
const filterWithAudio = `[1:v]scale=iw*${scale}:ih*${scale}[ov];[0:v][ov]overlay=${x}:${y}[outv];` +
|
|
1254
|
-
`[0:a]volume=1.0[a0];[1:a]volume=${vol}[a1];[a0][a1]amix=inputs=2:duration=first[outa]`;
|
|
1255
|
-
const transformed = (await ffmpegTransform(mainVideo, [
|
|
1256
|
-
"-filter_complex",
|
|
1257
|
-
filterWithAudio,
|
|
1258
|
-
"-map",
|
|
1259
|
-
"[outv]",
|
|
1260
|
-
"-map",
|
|
1261
|
-
"[outa]"
|
|
1262
|
-
], [{ suffix: ".mp4", bytes: overlayVideo }])) ??
|
|
1263
|
-
// Fallback: video-only overlay (one or both inputs may lack audio)
|
|
1264
|
-
(await ffmpegTransform(mainVideo, [
|
|
1265
|
-
"-filter_complex",
|
|
1266
|
-
`[1:v]scale=iw*${scale}:ih*${scale}[ov];[0:v][ov]overlay=${x}:${y}`
|
|
1267
|
-
], [{ suffix: ".mp4", bytes: overlayVideo }])) ??
|
|
1268
|
-
mainVideo;
|
|
1269
|
-
return { output: videoRef(transformed) };
|
|
1270
|
-
}
|
|
1271
|
-
}
|
|
1272
|
-
__decorate([
|
|
1273
|
-
prop({
|
|
1274
|
-
type: "video",
|
|
1275
|
-
default: {
|
|
1276
|
-
type: "video",
|
|
1277
|
-
uri: "",
|
|
1278
|
-
asset_id: null,
|
|
1279
|
-
data: null,
|
|
1280
|
-
metadata: null,
|
|
1281
|
-
duration: null,
|
|
1282
|
-
format: null
|
|
1283
|
-
},
|
|
1284
|
-
title: "Main Video",
|
|
1285
|
-
description: "The main (background) video."
|
|
1286
|
-
})
|
|
1287
|
-
], OverlayVideoNode.prototype, "main_video", void 0);
|
|
1288
|
-
__decorate([
|
|
1289
|
-
prop({
|
|
1290
|
-
type: "video",
|
|
1291
|
-
default: {
|
|
1292
|
-
type: "video",
|
|
1293
|
-
uri: "",
|
|
1294
|
-
asset_id: null,
|
|
1295
|
-
data: null,
|
|
1296
|
-
metadata: null,
|
|
1297
|
-
duration: null,
|
|
1298
|
-
format: null
|
|
1299
|
-
},
|
|
1300
|
-
title: "Overlay Video",
|
|
1301
|
-
description: "The video to overlay on top."
|
|
1302
|
-
})
|
|
1303
|
-
], OverlayVideoNode.prototype, "overlay_video", void 0);
|
|
1304
|
-
__decorate([
|
|
1305
|
-
prop({
|
|
1306
|
-
type: "int",
|
|
1307
|
-
default: 0,
|
|
1308
|
-
title: "X",
|
|
1309
|
-
description: "X-coordinate for overlay placement."
|
|
1310
|
-
})
|
|
1311
|
-
], OverlayVideoNode.prototype, "x", void 0);
|
|
1312
|
-
__decorate([
|
|
1313
|
-
prop({
|
|
1314
|
-
type: "int",
|
|
1315
|
-
default: 0,
|
|
1316
|
-
title: "Y",
|
|
1317
|
-
description: "Y-coordinate for overlay placement."
|
|
1318
|
-
})
|
|
1319
|
-
], OverlayVideoNode.prototype, "y", void 0);
|
|
1320
|
-
__decorate([
|
|
1321
|
-
prop({
|
|
1322
|
-
type: "float",
|
|
1323
|
-
default: 1,
|
|
1324
|
-
title: "Scale",
|
|
1325
|
-
description: "Scale factor for the overlay video."
|
|
1326
|
-
})
|
|
1327
|
-
], OverlayVideoNode.prototype, "scale", void 0);
|
|
1328
|
-
__decorate([
|
|
1329
|
-
prop({
|
|
1330
|
-
type: "float",
|
|
1331
|
-
default: 0.5,
|
|
1332
|
-
title: "Overlay Audio Volume",
|
|
1333
|
-
description: "Volume of the overlay audio relative to the main audio.",
|
|
1334
|
-
min: 0,
|
|
1335
|
-
max: 1
|
|
1336
|
-
})
|
|
1337
|
-
], OverlayVideoNode.prototype, "overlay_audio_volume", void 0);
|
|
1338
|
-
export class ColorBalanceVideoNode extends VideoTransformNode {
|
|
1339
|
-
static nodeType = "nodetool.video.ColorBalance";
|
|
1340
|
-
static title = "Color Balance";
|
|
1341
|
-
static description = "Adjust the color balance of a video.\n video, color, balance, adjustment";
|
|
1342
|
-
static metadataOutputTypes = {
|
|
1343
|
-
output: "video"
|
|
1344
|
-
};
|
|
1345
|
-
async process(context) {
|
|
1346
|
-
const bytes = await videoBytesAsync(this.video, context);
|
|
1347
|
-
// Props are 0-2 range where 1.0 = neutral; colorbalance expects -1 to 1
|
|
1348
|
-
const rs = Number(this.red_adjust ?? 1) - 1;
|
|
1349
|
-
const gs = Number(this.green_adjust ?? 1) - 1;
|
|
1350
|
-
const bs = Number(this.blue_adjust ?? 1) - 1;
|
|
1351
|
-
const transformed = (await ffmpegTransform(bytes, [
|
|
1352
|
-
"-vf",
|
|
1353
|
-
`colorbalance=rs=${rs}:gs=${gs}:bs=${bs}`,
|
|
1354
|
-
"-c:a",
|
|
1355
|
-
"copy"
|
|
1356
|
-
])) ?? bytes;
|
|
1357
|
-
return { output: videoRef(transformed) };
|
|
1358
|
-
}
|
|
1359
|
-
}
|
|
1360
|
-
__decorate([
|
|
1361
|
-
prop({
|
|
1362
|
-
type: "video",
|
|
1363
|
-
default: {
|
|
1364
|
-
type: "video",
|
|
1365
|
-
uri: "",
|
|
1366
|
-
asset_id: null,
|
|
1367
|
-
data: null,
|
|
1368
|
-
metadata: null,
|
|
1369
|
-
duration: null,
|
|
1370
|
-
format: null
|
|
1371
|
-
},
|
|
1372
|
-
title: "Video",
|
|
1373
|
-
description: "The input video to adjust color balance."
|
|
1374
|
-
})
|
|
1375
|
-
], ColorBalanceVideoNode.prototype, "video", void 0);
|
|
1376
|
-
__decorate([
|
|
1377
|
-
prop({
|
|
1378
|
-
type: "float",
|
|
1379
|
-
default: 1,
|
|
1380
|
-
title: "Red Adjust",
|
|
1381
|
-
description: "Red channel adjustment factor.",
|
|
1382
|
-
min: 0,
|
|
1383
|
-
max: 2
|
|
1384
|
-
})
|
|
1385
|
-
], ColorBalanceVideoNode.prototype, "red_adjust", void 0);
|
|
1386
|
-
__decorate([
|
|
1387
|
-
prop({
|
|
1388
|
-
type: "float",
|
|
1389
|
-
default: 1,
|
|
1390
|
-
title: "Green Adjust",
|
|
1391
|
-
description: "Green channel adjustment factor.",
|
|
1392
|
-
min: 0,
|
|
1393
|
-
max: 2
|
|
1394
|
-
})
|
|
1395
|
-
], ColorBalanceVideoNode.prototype, "green_adjust", void 0);
|
|
1396
|
-
__decorate([
|
|
1397
|
-
prop({
|
|
1398
|
-
type: "float",
|
|
1399
|
-
default: 1,
|
|
1400
|
-
title: "Blue Adjust",
|
|
1401
|
-
description: "Blue channel adjustment factor.",
|
|
1402
|
-
min: 0,
|
|
1403
|
-
max: 2
|
|
1404
|
-
})
|
|
1405
|
-
], ColorBalanceVideoNode.prototype, "blue_adjust", void 0);
|
|
1406
|
-
export class DenoiseVideoNode extends VideoTransformNode {
|
|
1407
|
-
static nodeType = "nodetool.video.Denoise";
|
|
1408
|
-
static title = "Denoise";
|
|
1409
|
-
static description = "Apply noise reduction to a video.\n video, denoise, clean, enhance";
|
|
1410
|
-
static metadataOutputTypes = {
|
|
1411
|
-
output: "video"
|
|
1412
|
-
};
|
|
1413
|
-
async process(context) {
|
|
1414
|
-
const bytes = await videoBytesAsync(this.video, context);
|
|
1415
|
-
const strength = Number(this.strength ?? 5);
|
|
1416
|
-
const transformed = (await ffmpegTransform(bytes, [
|
|
1417
|
-
"-vf",
|
|
1418
|
-
`nlmeans=s=${strength}`,
|
|
1419
|
-
"-c:a",
|
|
1420
|
-
"copy"
|
|
1421
|
-
])) ?? bytes;
|
|
1422
|
-
return { output: videoRef(transformed) };
|
|
1423
|
-
}
|
|
1424
|
-
}
|
|
1425
|
-
__decorate([
|
|
1426
|
-
prop({
|
|
1427
|
-
type: "video",
|
|
1428
|
-
default: {
|
|
1429
|
-
type: "video",
|
|
1430
|
-
uri: "",
|
|
1431
|
-
asset_id: null,
|
|
1432
|
-
data: null,
|
|
1433
|
-
metadata: null,
|
|
1434
|
-
duration: null,
|
|
1435
|
-
format: null
|
|
1436
|
-
},
|
|
1437
|
-
title: "Video",
|
|
1438
|
-
description: "The input video to denoise."
|
|
1439
|
-
})
|
|
1440
|
-
], DenoiseVideoNode.prototype, "video", void 0);
|
|
1441
|
-
__decorate([
|
|
1442
|
-
prop({
|
|
1443
|
-
type: "float",
|
|
1444
|
-
default: 5,
|
|
1445
|
-
title: "Strength",
|
|
1446
|
-
description: "Strength of the denoising effect. Higher values mean more denoising.",
|
|
1447
|
-
min: 0,
|
|
1448
|
-
max: 20
|
|
1449
|
-
})
|
|
1450
|
-
], DenoiseVideoNode.prototype, "strength", void 0);
|
|
1451
|
-
export class StabilizeVideoNode extends VideoTransformNode {
|
|
1452
|
-
static nodeType = "nodetool.video.Stabilize";
|
|
1453
|
-
static title = "Stabilize";
|
|
1454
|
-
static description = "Apply video stabilization to reduce camera shake and jitter.\n video, stabilize, smooth, shake-reduction";
|
|
1455
|
-
static metadataOutputTypes = {
|
|
1456
|
-
output: "video"
|
|
1457
|
-
};
|
|
1458
|
-
async process(context) {
|
|
1459
|
-
const bytes = await videoBytesAsync(this.video, context);
|
|
1460
|
-
const smoothing = Math.max(1, Number(this.smoothing ?? 10));
|
|
1461
|
-
const cropBlack = Boolean(this.crop_black ?? true);
|
|
1462
|
-
// deshake with smoothing parameter
|
|
1463
|
-
let vf = `deshake=smooth=${smoothing}`;
|
|
1464
|
-
// If crop_black is enabled, chain cropdetect and crop to remove black borders
|
|
1465
|
-
if (cropBlack) {
|
|
1466
|
-
vf += ",cropdetect=24:16:0,crop";
|
|
1467
|
-
}
|
|
1468
|
-
const transformed = (await ffmpegTransform(bytes, ["-vf", vf, "-c:a", "copy"])) ?? bytes;
|
|
1469
|
-
return { output: videoRef(transformed) };
|
|
1470
|
-
}
|
|
1471
|
-
}
|
|
1472
|
-
__decorate([
|
|
1473
|
-
prop({
|
|
1474
|
-
type: "video",
|
|
1475
|
-
default: {
|
|
1476
|
-
type: "video",
|
|
1477
|
-
uri: "",
|
|
1478
|
-
asset_id: null,
|
|
1479
|
-
data: null,
|
|
1480
|
-
metadata: null,
|
|
1481
|
-
duration: null,
|
|
1482
|
-
format: null
|
|
1483
|
-
},
|
|
1484
|
-
title: "Video",
|
|
1485
|
-
description: "The input video to stabilize."
|
|
1486
|
-
})
|
|
1487
|
-
], StabilizeVideoNode.prototype, "video", void 0);
|
|
1488
|
-
__decorate([
|
|
1489
|
-
prop({
|
|
1490
|
-
type: "float",
|
|
1491
|
-
default: 10,
|
|
1492
|
-
title: "Smoothing",
|
|
1493
|
-
description: "Smoothing strength. Higher values result in smoother but potentially more cropped video.",
|
|
1494
|
-
min: 1,
|
|
1495
|
-
max: 100
|
|
1496
|
-
})
|
|
1497
|
-
], StabilizeVideoNode.prototype, "smoothing", void 0);
|
|
1498
|
-
__decorate([
|
|
1499
|
-
prop({
|
|
1500
|
-
type: "bool",
|
|
1501
|
-
default: true,
|
|
1502
|
-
title: "Crop Black",
|
|
1503
|
-
description: "Whether to crop black borders that may appear after stabilization."
|
|
1504
|
-
})
|
|
1505
|
-
], StabilizeVideoNode.prototype, "crop_black", void 0);
|
|
1506
|
-
export class SharpnessVideoNode extends VideoTransformNode {
|
|
1507
|
-
static nodeType = "nodetool.video.Sharpness";
|
|
1508
|
-
static title = "Sharpness";
|
|
1509
|
-
static description = "Adjust the sharpness of a video.\n video, sharpen, enhance, detail";
|
|
1510
|
-
static metadataOutputTypes = {
|
|
1511
|
-
output: "video"
|
|
1512
|
-
};
|
|
1513
|
-
async process(context) {
|
|
1514
|
-
const bytes = await videoBytesAsync(this.video, context);
|
|
1515
|
-
const lumaAmount = Number(this.luma_amount ?? 1);
|
|
1516
|
-
const chromaAmount = Number(this.chroma_amount ?? 0.5);
|
|
1517
|
-
const transformed = (await ffmpegTransform(bytes, [
|
|
1518
|
-
"-vf",
|
|
1519
|
-
`unsharp=5:5:${lumaAmount}:5:5:${chromaAmount}`,
|
|
1520
|
-
"-c:a",
|
|
1521
|
-
"copy"
|
|
1522
|
-
])) ?? bytes;
|
|
1523
|
-
return { output: videoRef(transformed) };
|
|
1524
|
-
}
|
|
1525
|
-
}
|
|
1526
|
-
__decorate([
|
|
1527
|
-
prop({
|
|
1528
|
-
type: "video",
|
|
1529
|
-
default: {
|
|
1530
|
-
type: "video",
|
|
1531
|
-
uri: "",
|
|
1532
|
-
asset_id: null,
|
|
1533
|
-
data: null,
|
|
1534
|
-
metadata: null,
|
|
1535
|
-
duration: null,
|
|
1536
|
-
format: null
|
|
1537
|
-
},
|
|
1538
|
-
title: "Video",
|
|
1539
|
-
description: "The input video to sharpen."
|
|
1540
|
-
})
|
|
1541
|
-
], SharpnessVideoNode.prototype, "video", void 0);
|
|
1542
|
-
__decorate([
|
|
1543
|
-
prop({
|
|
1544
|
-
type: "float",
|
|
1545
|
-
default: 1,
|
|
1546
|
-
title: "Luma Amount",
|
|
1547
|
-
description: "Amount of sharpening to apply to luma (brightness) channel.",
|
|
1548
|
-
min: 0,
|
|
1549
|
-
max: 3
|
|
1550
|
-
})
|
|
1551
|
-
], SharpnessVideoNode.prototype, "luma_amount", void 0);
|
|
1552
|
-
__decorate([
|
|
1553
|
-
prop({
|
|
1554
|
-
type: "float",
|
|
1555
|
-
default: 0.5,
|
|
1556
|
-
title: "Chroma Amount",
|
|
1557
|
-
description: "Amount of sharpening to apply to chroma (color) channels.",
|
|
1558
|
-
min: 0,
|
|
1559
|
-
max: 3
|
|
1560
|
-
})
|
|
1561
|
-
], SharpnessVideoNode.prototype, "chroma_amount", void 0);
|
|
1562
|
-
export class BlurVideoNode extends VideoTransformNode {
|
|
1563
|
-
static nodeType = "nodetool.video.Blur";
|
|
1564
|
-
static title = "Blur";
|
|
1565
|
-
static description = "Apply a blur effect to a video.\n video, blur, smooth, soften";
|
|
1566
|
-
static metadataOutputTypes = {
|
|
1567
|
-
output: "video"
|
|
1568
|
-
};
|
|
1569
|
-
async process(context) {
|
|
1570
|
-
const bytes = await videoBytesAsync(this.video, context);
|
|
1571
|
-
const radius = Math.max(1, Number(this.strength ?? 2));
|
|
1572
|
-
const transformed = (await ffmpegTransform(bytes, [
|
|
1573
|
-
"-vf",
|
|
1574
|
-
`boxblur=${radius}:${radius}`,
|
|
1575
|
-
"-c:a",
|
|
1576
|
-
"copy"
|
|
1577
|
-
])) ?? bytes;
|
|
1578
|
-
return { output: videoRef(transformed) };
|
|
1579
|
-
}
|
|
1580
|
-
}
|
|
1581
|
-
__decorate([
|
|
1582
|
-
prop({
|
|
1583
|
-
type: "video",
|
|
1584
|
-
default: {
|
|
1585
|
-
type: "video",
|
|
1586
|
-
uri: "",
|
|
1587
|
-
asset_id: null,
|
|
1588
|
-
data: null,
|
|
1589
|
-
metadata: null,
|
|
1590
|
-
duration: null,
|
|
1591
|
-
format: null
|
|
1592
|
-
},
|
|
1593
|
-
title: "Video",
|
|
1594
|
-
description: "The input video to apply blur effect."
|
|
1595
|
-
})
|
|
1596
|
-
], BlurVideoNode.prototype, "video", void 0);
|
|
1597
|
-
__decorate([
|
|
1598
|
-
prop({
|
|
1599
|
-
type: "float",
|
|
1600
|
-
default: 5,
|
|
1601
|
-
title: "Strength",
|
|
1602
|
-
description: "The strength of the blur effect. Higher values create a stronger blur.",
|
|
1603
|
-
min: 0,
|
|
1604
|
-
max: 20
|
|
1605
|
-
})
|
|
1606
|
-
], BlurVideoNode.prototype, "strength", void 0);
|
|
1607
|
-
export class SaturationVideoNode extends VideoTransformNode {
|
|
1608
|
-
static nodeType = "nodetool.video.Saturation";
|
|
1609
|
-
static title = "Saturation";
|
|
1610
|
-
static description = "Adjust the color saturation of a video.\n video, saturation, color, enhance";
|
|
1611
|
-
static metadataOutputTypes = {
|
|
1612
|
-
output: "video"
|
|
1613
|
-
};
|
|
1614
|
-
async process(context) {
|
|
1615
|
-
const bytes = await videoBytesAsync(this.video, context);
|
|
1616
|
-
const saturation = Number(this.saturation ?? 1.2);
|
|
1617
|
-
const transformed = (await ffmpegTransform(bytes, [
|
|
1618
|
-
"-vf",
|
|
1619
|
-
`eq=saturation=${saturation}`,
|
|
1620
|
-
"-c:a",
|
|
1621
|
-
"copy"
|
|
1622
|
-
])) ?? bytes;
|
|
1623
|
-
return { output: videoRef(transformed) };
|
|
1624
|
-
}
|
|
1625
|
-
}
|
|
1626
|
-
__decorate([
|
|
1627
|
-
prop({
|
|
1628
|
-
type: "video",
|
|
1629
|
-
default: {
|
|
1630
|
-
type: "video",
|
|
1631
|
-
uri: "",
|
|
1632
|
-
asset_id: null,
|
|
1633
|
-
data: null,
|
|
1634
|
-
metadata: null,
|
|
1635
|
-
duration: null,
|
|
1636
|
-
format: null
|
|
1637
|
-
},
|
|
1638
|
-
title: "Video",
|
|
1639
|
-
description: "The input video to adjust saturation."
|
|
1640
|
-
})
|
|
1641
|
-
], SaturationVideoNode.prototype, "video", void 0);
|
|
1642
|
-
__decorate([
|
|
1643
|
-
prop({
|
|
1644
|
-
type: "float",
|
|
1645
|
-
default: 1,
|
|
1646
|
-
title: "Saturation",
|
|
1647
|
-
description: "Saturation level. 1.0 is original, <1 decreases saturation, >1 increases saturation.",
|
|
1648
|
-
min: 0,
|
|
1649
|
-
max: 3
|
|
1650
|
-
})
|
|
1651
|
-
], SaturationVideoNode.prototype, "saturation", void 0);
|
|
1652
|
-
export class AddSubtitlesVideoNode extends VideoTransformNode {
|
|
1653
|
-
static nodeType = "nodetool.video.AddSubtitles";
|
|
1654
|
-
static title = "Add Subtitles";
|
|
1655
|
-
static description = "Add subtitles to a video.\n video, subtitles, text, caption";
|
|
1656
|
-
static metadataOutputTypes = {
|
|
1657
|
-
output: "video"
|
|
1658
|
-
};
|
|
1659
|
-
async process(context) {
|
|
1660
|
-
const bytes = await videoBytesAsync(this.video, context);
|
|
1661
|
-
const chunks = Array.isArray(this.chunks) ? this.chunks : [];
|
|
1662
|
-
if (chunks.length === 0)
|
|
1663
|
-
return { output: videoRef(bytes) };
|
|
1664
|
-
const fontSize = Math.max(1, Number(this.font_size ?? 24));
|
|
1665
|
-
const fontColorObj = this.font_color;
|
|
1666
|
-
const fontColor = String(fontColorObj?.value ?? "#FFFFFF").replace("#", "0x");
|
|
1667
|
-
const fontName = String(this.font?.name ?? "Sans");
|
|
1668
|
-
const align = String(this.align ?? "bottom");
|
|
1669
|
-
// Compute y position based on alignment
|
|
1670
|
-
let yExpr;
|
|
1671
|
-
if (align === "top") {
|
|
1672
|
-
yExpr = "text_h";
|
|
1673
|
-
}
|
|
1674
|
-
else if (align === "center") {
|
|
1675
|
-
yExpr = "(h-text_h)/2";
|
|
1676
|
-
}
|
|
1677
|
-
else {
|
|
1678
|
-
// bottom
|
|
1679
|
-
yExpr = "h-(text_h*2)";
|
|
1680
|
-
}
|
|
1681
|
-
// Build chained drawtext filters, one per chunk with enable=between(t,start,end)
|
|
1682
|
-
const drawFilters = chunks
|
|
1683
|
-
.map((chunk) => {
|
|
1684
|
-
const text = String(chunk.text ?? chunk.content ?? "").trim();
|
|
1685
|
-
if (!text)
|
|
1686
|
-
return null;
|
|
1687
|
-
const ts = Array.isArray(chunk.timestamp) ? chunk.timestamp : [];
|
|
1688
|
-
const start = Number(ts[0] ?? chunk.start ?? 0);
|
|
1689
|
-
const end = Number(ts[1] ?? chunk.end ?? start + 5);
|
|
1690
|
-
const escaped = text
|
|
1691
|
-
.replaceAll("\\", "\\\\")
|
|
1692
|
-
.replaceAll(":", "\\:")
|
|
1693
|
-
.replaceAll("'", "\u2019")
|
|
1694
|
-
.replaceAll("%", "%%");
|
|
1695
|
-
return (`drawtext=text='${escaped}'` +
|
|
1696
|
-
`:x=(w-text_w)/2:y=${yExpr}` +
|
|
1697
|
-
`:fontcolor=${fontColor}:fontsize=${fontSize}` +
|
|
1698
|
-
`:font='${fontName}'` +
|
|
1699
|
-
`:enable='between(t,${start},${end})'`);
|
|
1700
|
-
})
|
|
1701
|
-
.filter(Boolean);
|
|
1702
|
-
if (drawFilters.length === 0)
|
|
1703
|
-
return { output: videoRef(bytes) };
|
|
1704
|
-
const vf = drawFilters.join(",");
|
|
1705
|
-
const transformed = (await ffmpegTransform(bytes, ["-vf", vf, "-c:a", "copy"])) ?? bytes;
|
|
1706
|
-
return { output: videoRef(transformed) };
|
|
1707
|
-
}
|
|
1708
|
-
}
|
|
1709
|
-
__decorate([
|
|
1710
|
-
prop({
|
|
1711
|
-
type: "video",
|
|
1712
|
-
default: {
|
|
1713
|
-
type: "video",
|
|
1714
|
-
uri: "",
|
|
1715
|
-
asset_id: null,
|
|
1716
|
-
data: null,
|
|
1717
|
-
metadata: null,
|
|
1718
|
-
duration: null,
|
|
1719
|
-
format: null
|
|
1720
|
-
},
|
|
1721
|
-
title: "Video",
|
|
1722
|
-
description: "The input video to add subtitles to."
|
|
1723
|
-
})
|
|
1724
|
-
], AddSubtitlesVideoNode.prototype, "video", void 0);
|
|
1725
|
-
__decorate([
|
|
1726
|
-
prop({
|
|
1727
|
-
type: "list[audio_chunk]",
|
|
1728
|
-
default: [],
|
|
1729
|
-
title: "Chunks",
|
|
1730
|
-
description: "Audio chunks to add as subtitles."
|
|
1731
|
-
})
|
|
1732
|
-
], AddSubtitlesVideoNode.prototype, "chunks", void 0);
|
|
1733
|
-
__decorate([
|
|
1734
|
-
prop({
|
|
1735
|
-
type: "font",
|
|
1736
|
-
default: {
|
|
1737
|
-
type: "font",
|
|
1738
|
-
name: "",
|
|
1739
|
-
source: "system",
|
|
1740
|
-
url: "",
|
|
1741
|
-
weight: "regular"
|
|
1742
|
-
},
|
|
1743
|
-
title: "Font",
|
|
1744
|
-
description: "The font to use."
|
|
1745
|
-
})
|
|
1746
|
-
], AddSubtitlesVideoNode.prototype, "font", void 0);
|
|
1747
|
-
__decorate([
|
|
1748
|
-
prop({
|
|
1749
|
-
type: "enum",
|
|
1750
|
-
default: "bottom",
|
|
1751
|
-
title: "Align",
|
|
1752
|
-
description: "Vertical alignment of subtitles.",
|
|
1753
|
-
values: ["top", "center", "bottom"]
|
|
1754
|
-
})
|
|
1755
|
-
], AddSubtitlesVideoNode.prototype, "align", void 0);
|
|
1756
|
-
__decorate([
|
|
1757
|
-
prop({
|
|
1758
|
-
type: "int",
|
|
1759
|
-
default: 24,
|
|
1760
|
-
title: "Font Size",
|
|
1761
|
-
description: "The font size.",
|
|
1762
|
-
min: 1,
|
|
1763
|
-
max: 72
|
|
1764
|
-
})
|
|
1765
|
-
], AddSubtitlesVideoNode.prototype, "font_size", void 0);
|
|
1766
|
-
__decorate([
|
|
1767
|
-
prop({
|
|
1768
|
-
type: "color",
|
|
1769
|
-
default: {
|
|
1770
|
-
type: "color",
|
|
1771
|
-
value: "#FFFFFF"
|
|
1772
|
-
},
|
|
1773
|
-
title: "Font Color",
|
|
1774
|
-
description: "The font color."
|
|
1775
|
-
})
|
|
1776
|
-
], AddSubtitlesVideoNode.prototype, "font_color", void 0);
|
|
1777
|
-
export class ReverseVideoNode extends BaseNode {
|
|
1778
|
-
static nodeType = "nodetool.video.Reverse";
|
|
1779
|
-
static requiredRuntimes = ["ffmpeg"];
|
|
1780
|
-
static title = "Reverse";
|
|
1781
|
-
static description = "Reverse the playback of a video.\n video, reverse, backwards, effect";
|
|
1782
|
-
static metadataOutputTypes = {
|
|
1783
|
-
output: "video"
|
|
1784
|
-
};
|
|
1785
|
-
async process(context) {
|
|
1786
|
-
const bytes = await videoBytesAsync(this.video, context);
|
|
1787
|
-
if (bytes.length === 0)
|
|
1788
|
-
return { output: videoRef(bytes) };
|
|
1789
|
-
// Try with both video and audio reverse, fall back to video-only
|
|
1790
|
-
const transformed = (await ffmpegTransform(bytes, ["-vf", "reverse", "-af", "areverse"])) ??
|
|
1791
|
-
(await ffmpegTransform(bytes, ["-vf", "reverse"])) ??
|
|
1792
|
-
bytes;
|
|
1793
|
-
return { output: videoRef(transformed) };
|
|
1794
|
-
}
|
|
1795
|
-
}
|
|
1796
|
-
__decorate([
|
|
1797
|
-
prop({
|
|
1798
|
-
type: "video",
|
|
1799
|
-
default: {
|
|
1800
|
-
type: "video",
|
|
1801
|
-
uri: "",
|
|
1802
|
-
asset_id: null,
|
|
1803
|
-
data: null,
|
|
1804
|
-
metadata: null,
|
|
1805
|
-
duration: null,
|
|
1806
|
-
format: null
|
|
1807
|
-
},
|
|
1808
|
-
title: "Video",
|
|
1809
|
-
description: "The input video to reverse."
|
|
1810
|
-
})
|
|
1811
|
-
], ReverseVideoNode.prototype, "video", void 0);
|
|
1812
|
-
export class TransitionVideoNode extends VideoTransformNode {
|
|
1813
|
-
static nodeType = "nodetool.video.Transition";
|
|
1814
|
-
static title = "Transition";
|
|
1815
|
-
static description = "Create a transition effect between two videos, including audio transition when available.\n video, transition, effect, merge, audio";
|
|
1816
|
-
static metadataOutputTypes = {
|
|
1817
|
-
output: "video"
|
|
1818
|
-
};
|
|
1819
|
-
async process(context) {
|
|
1820
|
-
const a = await videoBytesAsync(this.video_a, context);
|
|
1821
|
-
const b = await videoBytesAsync(this.video_b, context);
|
|
1822
|
-
if (b.length === 0)
|
|
1823
|
-
return { output: videoRef(a) };
|
|
1824
|
-
const duration = Math.max(0.1, Number(this.duration ?? 1));
|
|
1825
|
-
const transitionType = String(this.transition_type ?? "fade");
|
|
1826
|
-
// Probe video_a duration to compute offset (transition starts at end of video_a)
|
|
1827
|
-
const tempA = await withTempFile(".mp4", a);
|
|
1828
|
-
let offsetVal;
|
|
1829
|
-
try {
|
|
1830
|
-
const aDuration = await ffprobeDuration(tempA.path);
|
|
1831
|
-
offsetVal = Math.max(0, aDuration - duration);
|
|
1832
|
-
}
|
|
1833
|
-
finally {
|
|
1834
|
-
await tempA.cleanup();
|
|
1835
|
-
}
|
|
1836
|
-
// Try with audio crossfade first, then fall back to video-only
|
|
1837
|
-
const filterWithAudio = `[0:v][1:v]xfade=transition=${transitionType}:duration=${duration}:offset=${offsetVal}[outv];` +
|
|
1838
|
-
`[0:a][1:a]acrossfade=d=${duration}[outa]`;
|
|
1839
|
-
const transformed = (await ffmpegTransform(a, [
|
|
1840
|
-
"-filter_complex",
|
|
1841
|
-
filterWithAudio,
|
|
1842
|
-
"-map",
|
|
1843
|
-
"[outv]",
|
|
1844
|
-
"-map",
|
|
1845
|
-
"[outa]"
|
|
1846
|
-
], [{ suffix: ".mp4", bytes: b }])) ??
|
|
1847
|
-
// Fallback: video-only xfade (inputs may lack audio)
|
|
1848
|
-
(await ffmpegTransform(a, [
|
|
1849
|
-
"-filter_complex",
|
|
1850
|
-
`[0:v][1:v]xfade=transition=${transitionType}:duration=${duration}:offset=${offsetVal}`
|
|
1851
|
-
], [{ suffix: ".mp4", bytes: b }])) ??
|
|
1852
|
-
videoBytes({ data: Buffer.from(bytesConcat([a, b])).toString("base64") });
|
|
1853
|
-
return { output: videoRef(transformed) };
|
|
1854
|
-
}
|
|
1855
|
-
}
|
|
1856
|
-
__decorate([
|
|
1857
|
-
prop({
|
|
1858
|
-
type: "video",
|
|
1859
|
-
default: {
|
|
1860
|
-
type: "video",
|
|
1861
|
-
uri: "",
|
|
1862
|
-
asset_id: null,
|
|
1863
|
-
data: null,
|
|
1864
|
-
metadata: null,
|
|
1865
|
-
duration: null,
|
|
1866
|
-
format: null
|
|
1867
|
-
},
|
|
1868
|
-
title: "Video A",
|
|
1869
|
-
description: "The first video in the transition."
|
|
1870
|
-
})
|
|
1871
|
-
], TransitionVideoNode.prototype, "video_a", void 0);
|
|
1872
|
-
__decorate([
|
|
1873
|
-
prop({
|
|
1874
|
-
type: "video",
|
|
1875
|
-
default: {
|
|
1876
|
-
type: "video",
|
|
1877
|
-
uri: "",
|
|
1878
|
-
asset_id: null,
|
|
1879
|
-
data: null,
|
|
1880
|
-
metadata: null,
|
|
1881
|
-
duration: null,
|
|
1882
|
-
format: null
|
|
1883
|
-
},
|
|
1884
|
-
title: "Video B",
|
|
1885
|
-
description: "The second video in the transition."
|
|
1886
|
-
})
|
|
1887
|
-
], TransitionVideoNode.prototype, "video_b", void 0);
|
|
1888
|
-
__decorate([
|
|
1889
|
-
prop({
|
|
1890
|
-
type: "enum",
|
|
1891
|
-
default: "fade",
|
|
1892
|
-
title: "Transition Type",
|
|
1893
|
-
description: "Type of transition effect",
|
|
1894
|
-
values: [
|
|
1895
|
-
"fade",
|
|
1896
|
-
"wipeleft",
|
|
1897
|
-
"wiperight",
|
|
1898
|
-
"wipeup",
|
|
1899
|
-
"wipedown",
|
|
1900
|
-
"slideleft",
|
|
1901
|
-
"slideright",
|
|
1902
|
-
"slideup",
|
|
1903
|
-
"slidedown",
|
|
1904
|
-
"circlecrop",
|
|
1905
|
-
"rectcrop",
|
|
1906
|
-
"distance",
|
|
1907
|
-
"fadeblack",
|
|
1908
|
-
"fadewhite",
|
|
1909
|
-
"radial",
|
|
1910
|
-
"smoothleft",
|
|
1911
|
-
"smoothright",
|
|
1912
|
-
"smoothup",
|
|
1913
|
-
"smoothdown",
|
|
1914
|
-
"circleopen",
|
|
1915
|
-
"circleclose",
|
|
1916
|
-
"vertopen",
|
|
1917
|
-
"vertclose",
|
|
1918
|
-
"horzopen",
|
|
1919
|
-
"horzclose",
|
|
1920
|
-
"dissolve",
|
|
1921
|
-
"pixelize",
|
|
1922
|
-
"diagtl",
|
|
1923
|
-
"diagtr",
|
|
1924
|
-
"diagbl",
|
|
1925
|
-
"diagbr",
|
|
1926
|
-
"hlslice",
|
|
1927
|
-
"hrslice",
|
|
1928
|
-
"vuslice",
|
|
1929
|
-
"vdslice",
|
|
1930
|
-
"hblur",
|
|
1931
|
-
"fadegrays",
|
|
1932
|
-
"wipetl",
|
|
1933
|
-
"wipetr",
|
|
1934
|
-
"wipebl",
|
|
1935
|
-
"wipebr",
|
|
1936
|
-
"squeezeh",
|
|
1937
|
-
"squeezev",
|
|
1938
|
-
"zoomin",
|
|
1939
|
-
"fadefast",
|
|
1940
|
-
"fadeslow",
|
|
1941
|
-
"hlwind",
|
|
1942
|
-
"hrwind",
|
|
1943
|
-
"vuwind",
|
|
1944
|
-
"vdwind",
|
|
1945
|
-
"coverleft",
|
|
1946
|
-
"coverright",
|
|
1947
|
-
"coverup",
|
|
1948
|
-
"coverdown",
|
|
1949
|
-
"revealleft",
|
|
1950
|
-
"revealright",
|
|
1951
|
-
"revealup",
|
|
1952
|
-
"revealdown"
|
|
1953
|
-
]
|
|
1954
|
-
})
|
|
1955
|
-
], TransitionVideoNode.prototype, "transition_type", void 0);
|
|
1956
|
-
__decorate([
|
|
1957
|
-
prop({
|
|
1958
|
-
type: "float",
|
|
1959
|
-
default: 1,
|
|
1960
|
-
title: "Duration",
|
|
1961
|
-
description: "Duration of the transition effect in seconds.",
|
|
1962
|
-
min: 0.1,
|
|
1963
|
-
max: 5
|
|
1964
|
-
})
|
|
1965
|
-
], TransitionVideoNode.prototype, "duration", void 0);
|
|
1966
|
-
export class AddAudioVideoNode extends BaseNode {
|
|
1967
|
-
static nodeType = "nodetool.video.AddAudio";
|
|
1968
|
-
static requiredRuntimes = ["ffmpeg"];
|
|
1969
|
-
static title = "Add Audio";
|
|
1970
|
-
static description = "Add an audio track to a video, replacing or mixing with existing audio.\n video, audio, soundtrack, merge";
|
|
1971
|
-
static metadataOutputTypes = {
|
|
1972
|
-
output: "video"
|
|
1973
|
-
};
|
|
1974
|
-
async process(context) {
|
|
1975
|
-
const v = await videoBytesAsync(this.video, context);
|
|
1976
|
-
const a = audioBytes(this.audio);
|
|
1977
|
-
if (v.length === 0)
|
|
1978
|
-
return { output: videoRef(v) };
|
|
1979
|
-
if (a.length === 0)
|
|
1980
|
-
return { output: videoRef(v) };
|
|
1981
|
-
const videoInput = await withTempFile(".mp4", v);
|
|
1982
|
-
const audioInput = await withTempFile(".wav", a);
|
|
1983
|
-
const outputDir = await fs.mkdtemp(path.join(os.tmpdir(), "nodetool-addaudio-out-"));
|
|
1984
|
-
const outputPath = path.join(outputDir, "output.mp4");
|
|
1985
|
-
try {
|
|
1986
|
-
const volume = Math.max(0, Number(this.volume ?? 1));
|
|
1987
|
-
const mix = Boolean(this.mix);
|
|
1988
|
-
const args = [
|
|
1989
|
-
"-y",
|
|
1990
|
-
"-i", videoInput.path,
|
|
1991
|
-
"-i", audioInput.path
|
|
1992
|
-
];
|
|
1993
|
-
if (mix) {
|
|
1994
|
-
// Mix new audio with existing audio
|
|
1995
|
-
args.push("-filter_complex", `[1:a]volume=${volume}[newaud];[0:a][newaud]amix=inputs=2:duration=first[aout]`, "-map", "0:v:0", "-map", "[aout]", "-c:v", "copy");
|
|
1996
|
-
}
|
|
1997
|
-
else {
|
|
1998
|
-
// Replace existing audio
|
|
1999
|
-
args.push("-map", "0:v:0", "-map", "1:a:0", "-c:v", "copy", "-c:a", "aac");
|
|
2000
|
-
if (volume !== 1) {
|
|
2001
|
-
// Insert volume filter before output
|
|
2002
|
-
args.splice(args.length, 0);
|
|
2003
|
-
// Re-build with filter
|
|
2004
|
-
args.length = 0;
|
|
2005
|
-
args.push("-y", "-i", videoInput.path, "-i", audioInput.path, "-filter_complex", `[1:a]volume=${volume}[aout]`, "-map", "0:v:0", "-map", "[aout]", "-c:v", "copy", "-c:a", "aac");
|
|
2006
|
-
}
|
|
2007
|
-
}
|
|
2008
|
-
args.push(outputPath);
|
|
2009
|
-
await execFile("ffmpeg", args, { maxBuffer: 50 * 1024 * 1024 });
|
|
2010
|
-
const result = new Uint8Array(await fs.readFile(outputPath));
|
|
2011
|
-
return { output: videoRef(result) };
|
|
2012
|
-
}
|
|
2013
|
-
catch {
|
|
2014
|
-
return { output: videoRef(v) };
|
|
2015
|
-
}
|
|
2016
|
-
finally {
|
|
2017
|
-
await videoInput.cleanup();
|
|
2018
|
-
await audioInput.cleanup();
|
|
2019
|
-
await fs.rm(outputDir, { recursive: true, force: true });
|
|
2020
|
-
}
|
|
2021
|
-
}
|
|
2022
|
-
}
|
|
2023
|
-
__decorate([
|
|
2024
|
-
prop({
|
|
2025
|
-
type: "video",
|
|
2026
|
-
default: {
|
|
2027
|
-
type: "video",
|
|
2028
|
-
uri: "",
|
|
2029
|
-
asset_id: null,
|
|
2030
|
-
data: null,
|
|
2031
|
-
metadata: null,
|
|
2032
|
-
duration: null,
|
|
2033
|
-
format: null
|
|
2034
|
-
},
|
|
2035
|
-
title: "Video",
|
|
2036
|
-
description: "The input video to add audio to."
|
|
2037
|
-
})
|
|
2038
|
-
], AddAudioVideoNode.prototype, "video", void 0);
|
|
2039
|
-
__decorate([
|
|
2040
|
-
prop({
|
|
2041
|
-
type: "audio",
|
|
2042
|
-
default: {
|
|
2043
|
-
type: "audio",
|
|
2044
|
-
uri: "",
|
|
2045
|
-
asset_id: null,
|
|
2046
|
-
data: null,
|
|
2047
|
-
metadata: null
|
|
2048
|
-
},
|
|
2049
|
-
title: "Audio",
|
|
2050
|
-
description: "The audio file to add to the video."
|
|
2051
|
-
})
|
|
2052
|
-
], AddAudioVideoNode.prototype, "audio", void 0);
|
|
2053
|
-
__decorate([
|
|
2054
|
-
prop({
|
|
2055
|
-
type: "float",
|
|
2056
|
-
default: 1,
|
|
2057
|
-
title: "Volume",
|
|
2058
|
-
description: "Volume adjustment for the added audio. 1.0 is original volume.",
|
|
2059
|
-
min: 0,
|
|
2060
|
-
max: 2
|
|
2061
|
-
})
|
|
2062
|
-
], AddAudioVideoNode.prototype, "volume", void 0);
|
|
2063
|
-
__decorate([
|
|
2064
|
-
prop({
|
|
2065
|
-
type: "bool",
|
|
2066
|
-
default: false,
|
|
2067
|
-
title: "Mix",
|
|
2068
|
-
description: "If True, mix new audio with existing. If False, replace existing audio."
|
|
2069
|
-
})
|
|
2070
|
-
], AddAudioVideoNode.prototype, "mix", void 0);
|
|
2071
|
-
export class ChromaKeyVideoNode extends VideoTransformNode {
|
|
2072
|
-
static nodeType = "nodetool.video.ChromaKey";
|
|
2073
|
-
static title = "Chroma Key";
|
|
2074
|
-
static description = "Apply chroma key (green screen) effect to a video.\n video, chroma key, green screen, compositing";
|
|
2075
|
-
static metadataOutputTypes = {
|
|
2076
|
-
output: "video"
|
|
2077
|
-
};
|
|
2078
|
-
async process(context) {
|
|
2079
|
-
const bytes = await videoBytesAsync(this.video, context);
|
|
2080
|
-
const color = String(this.key_color ?? "0x00FF00");
|
|
2081
|
-
const similarity = Number(this.similarity ?? 0.1);
|
|
2082
|
-
const blend = Number(this.blend ?? 0.0);
|
|
2083
|
-
const transformed = (await ffmpegTransform(bytes, [
|
|
2084
|
-
"-vf",
|
|
2085
|
-
`colorkey=${color}:${similarity}:${blend}`,
|
|
2086
|
-
"-c:a",
|
|
2087
|
-
"copy"
|
|
2088
|
-
])) ?? bytes;
|
|
2089
|
-
return { output: videoRef(transformed) };
|
|
2090
|
-
}
|
|
2091
|
-
}
|
|
2092
|
-
__decorate([
|
|
2093
|
-
prop({
|
|
2094
|
-
type: "video",
|
|
2095
|
-
default: {
|
|
2096
|
-
type: "video",
|
|
2097
|
-
uri: "",
|
|
2098
|
-
asset_id: null,
|
|
2099
|
-
data: null,
|
|
2100
|
-
metadata: null,
|
|
2101
|
-
duration: null,
|
|
2102
|
-
format: null
|
|
2103
|
-
},
|
|
2104
|
-
title: "Video",
|
|
2105
|
-
description: "The input video to apply chroma key effect."
|
|
2106
|
-
})
|
|
2107
|
-
], ChromaKeyVideoNode.prototype, "video", void 0);
|
|
2108
|
-
__decorate([
|
|
2109
|
-
prop({
|
|
2110
|
-
type: "color",
|
|
2111
|
-
default: {
|
|
2112
|
-
type: "color",
|
|
2113
|
-
value: "#00FF00"
|
|
2114
|
-
},
|
|
2115
|
-
title: "Key Color",
|
|
2116
|
-
description: "The color to key out (e.g., '#00FF00' for green)."
|
|
2117
|
-
})
|
|
2118
|
-
], ChromaKeyVideoNode.prototype, "key_color", void 0);
|
|
2119
|
-
__decorate([
|
|
2120
|
-
prop({
|
|
2121
|
-
type: "float",
|
|
2122
|
-
default: 0.3,
|
|
2123
|
-
title: "Similarity",
|
|
2124
|
-
description: "Similarity threshold for the key color.",
|
|
2125
|
-
min: 0,
|
|
2126
|
-
max: 1
|
|
2127
|
-
})
|
|
2128
|
-
], ChromaKeyVideoNode.prototype, "similarity", void 0);
|
|
2129
|
-
__decorate([
|
|
2130
|
-
prop({
|
|
2131
|
-
type: "float",
|
|
2132
|
-
default: 0.1,
|
|
2133
|
-
title: "Blend",
|
|
2134
|
-
description: "Blending of the keyed area edges.",
|
|
2135
|
-
min: 0,
|
|
2136
|
-
max: 1
|
|
2137
|
-
})
|
|
2138
|
-
], ChromaKeyVideoNode.prototype, "blend", void 0);
|
|
2139
|
-
export class ExtractAudioVideoNode extends BaseNode {
|
|
2140
|
-
static nodeType = "nodetool.video.ExtractAudio";
|
|
2141
|
-
static requiredRuntimes = ["ffmpeg"];
|
|
2142
|
-
static title = "Extract Audio";
|
|
2143
|
-
static description = "Separate and extract audio track from a video file.\n video, audio, extract, separate, split";
|
|
2144
|
-
static metadataOutputTypes = {
|
|
2145
|
-
output: "audio"
|
|
2146
|
-
};
|
|
2147
|
-
async process(context) {
|
|
2148
|
-
const bytes = await videoBytesAsync(this.video, context);
|
|
2149
|
-
if (bytes.length === 0)
|
|
2150
|
-
return { output: { data: null } };
|
|
2151
|
-
const input = await withTempFile(".mp4", bytes);
|
|
2152
|
-
const outputDir = await fs.mkdtemp(path.join(os.tmpdir(), "nodetool-audio-out-"));
|
|
2153
|
-
const outputPath = path.join(outputDir, "output.wav");
|
|
2154
|
-
try {
|
|
2155
|
-
await execFile("ffmpeg", ["-y", "-i", input.path, "-vn", "-acodec", "pcm_s16le", outputPath], { maxBuffer: 50 * 1024 * 1024 });
|
|
2156
|
-
const audioBytes = new Uint8Array(await fs.readFile(outputPath));
|
|
2157
|
-
const b64 = Buffer.from(audioBytes).toString("base64");
|
|
2158
|
-
return {
|
|
2159
|
-
output: {
|
|
2160
|
-
type: "audio",
|
|
2161
|
-
data: b64,
|
|
2162
|
-
uri: "",
|
|
2163
|
-
asset_id: null,
|
|
2164
|
-
metadata: null
|
|
2165
|
-
}
|
|
2166
|
-
};
|
|
2167
|
-
}
|
|
2168
|
-
finally {
|
|
2169
|
-
await input.cleanup();
|
|
2170
|
-
await fs.rm(outputDir, { recursive: true, force: true });
|
|
2171
|
-
}
|
|
2172
|
-
}
|
|
2173
|
-
}
|
|
2174
|
-
__decorate([
|
|
2175
|
-
prop({
|
|
2176
|
-
type: "video",
|
|
2177
|
-
default: {
|
|
2178
|
-
type: "video",
|
|
2179
|
-
uri: "",
|
|
2180
|
-
asset_id: null,
|
|
2181
|
-
data: null,
|
|
2182
|
-
metadata: null,
|
|
2183
|
-
duration: null,
|
|
2184
|
-
format: null
|
|
2185
|
-
},
|
|
2186
|
-
title: "Video",
|
|
2187
|
-
description: "The input video to separate."
|
|
2188
|
-
})
|
|
2189
|
-
], ExtractAudioVideoNode.prototype, "video", void 0);
|
|
2190
|
-
export class ExtractFrameVideoNode extends BaseNode {
|
|
2191
|
-
static nodeType = "nodetool.video.ExtractFrame";
|
|
2192
|
-
static requiredRuntimes = ["ffmpeg"];
|
|
2193
|
-
static title = "Extract Frame";
|
|
2194
|
-
static description = "Extract a single frame from a video at a specific time position.\n video, frame, extract, screenshot, thumbnail, capture";
|
|
2195
|
-
static metadataOutputTypes = {
|
|
2196
|
-
output: "image"
|
|
2197
|
-
};
|
|
2198
|
-
async process(context) {
|
|
2199
|
-
const bytes = await videoBytesAsync(this.video, context);
|
|
2200
|
-
if (bytes.length === 0) {
|
|
2201
|
-
return { output: { type: "image", data: null } };
|
|
2202
|
-
}
|
|
2203
|
-
const inputFile = await withTempFile(".mp4", bytes);
|
|
2204
|
-
const outputDir = await fs.mkdtemp(path.join(os.tmpdir(), "nodetool-extract-frame-"));
|
|
2205
|
-
const outputPath = path.join(outputDir, "frame.png");
|
|
2206
|
-
try {
|
|
2207
|
-
const time = Math.max(0, Number(this.time ?? 0));
|
|
2208
|
-
await execFile("ffmpeg", [
|
|
2209
|
-
"-y",
|
|
2210
|
-
"-ss", String(time),
|
|
2211
|
-
"-i", inputFile.path,
|
|
2212
|
-
"-frames:v", "1",
|
|
2213
|
-
"-f", "image2",
|
|
2214
|
-
outputPath
|
|
2215
|
-
], { maxBuffer: 10 * 1024 * 1024 });
|
|
2216
|
-
const frameData = new Uint8Array(await fs.readFile(outputPath));
|
|
2217
|
-
return {
|
|
2218
|
-
output: {
|
|
2219
|
-
type: "image",
|
|
2220
|
-
data: Buffer.from(frameData).toString("base64")
|
|
2221
|
-
}
|
|
2222
|
-
};
|
|
2223
|
-
}
|
|
2224
|
-
catch {
|
|
2225
|
-
return { output: { type: "image", data: null } };
|
|
2226
|
-
}
|
|
2227
|
-
finally {
|
|
2228
|
-
await inputFile.cleanup();
|
|
2229
|
-
await fs.rm(outputDir, { recursive: true, force: true });
|
|
2230
|
-
}
|
|
2231
|
-
}
|
|
2232
|
-
}
|
|
2233
|
-
__decorate([
|
|
2234
|
-
prop({
|
|
2235
|
-
type: "video",
|
|
2236
|
-
default: {
|
|
2237
|
-
type: "video",
|
|
2238
|
-
uri: "",
|
|
2239
|
-
asset_id: null,
|
|
2240
|
-
data: null,
|
|
2241
|
-
metadata: null,
|
|
2242
|
-
duration: null,
|
|
2243
|
-
format: null
|
|
2244
|
-
},
|
|
2245
|
-
title: "Video",
|
|
2246
|
-
description: "The input video to extract a frame from."
|
|
2247
|
-
})
|
|
2248
|
-
], ExtractFrameVideoNode.prototype, "video", void 0);
|
|
2249
|
-
__decorate([
|
|
2250
|
-
prop({
|
|
2251
|
-
type: "float",
|
|
2252
|
-
default: 0,
|
|
2253
|
-
title: "Time",
|
|
2254
|
-
description: "Time position in seconds to extract the frame from.",
|
|
2255
|
-
min: 0
|
|
2256
|
-
})
|
|
2257
|
-
], ExtractFrameVideoNode.prototype, "time", void 0);
|
|
2258
|
-
export class GetVideoInfoNode extends BaseNode {
|
|
2259
|
-
static nodeType = "nodetool.video.GetVideoInfo";
|
|
2260
|
-
static title = "Get Video Info";
|
|
2261
|
-
static description = "Get metadata information about a video file.\n Includes duration, resolution, frame rate, and codec.\n video, info, metadata, duration, resolution, fps, codec, analysis";
|
|
2262
|
-
static metadataOutputTypes = {
|
|
2263
|
-
duration: "float",
|
|
2264
|
-
width: "int",
|
|
2265
|
-
height: "int",
|
|
2266
|
-
fps: "float",
|
|
2267
|
-
frame_count: "int",
|
|
2268
|
-
codec: "str",
|
|
2269
|
-
has_audio: "bool"
|
|
2270
|
-
};
|
|
2271
|
-
async process(context) {
|
|
2272
|
-
const bytes = await videoBytesAsync(this.video, context);
|
|
2273
|
-
if (bytes.length === 0) {
|
|
2274
|
-
return {
|
|
2275
|
-
duration: 0,
|
|
2276
|
-
width: 0,
|
|
2277
|
-
height: 0,
|
|
2278
|
-
fps: 0,
|
|
2279
|
-
frame_count: 0,
|
|
2280
|
-
codec: "",
|
|
2281
|
-
has_audio: false
|
|
2282
|
-
};
|
|
2283
|
-
}
|
|
2284
|
-
const inputFile = await withTempFile(".mp4", bytes);
|
|
2285
|
-
try {
|
|
2286
|
-
const { stdout } = await execFile("ffprobe", [
|
|
2287
|
-
"-v", "error",
|
|
2288
|
-
"-show_streams",
|
|
2289
|
-
"-show_format",
|
|
2290
|
-
"-of", "json",
|
|
2291
|
-
inputFile.path
|
|
2292
|
-
]);
|
|
2293
|
-
const info = JSON.parse(stdout);
|
|
2294
|
-
const videoStream = (info.streams ?? []).find((s) => s.codec_type === "video");
|
|
2295
|
-
const hasAudio = (info.streams ?? []).some((s) => s.codec_type === "audio");
|
|
2296
|
-
let fps = 0;
|
|
2297
|
-
if (videoStream?.r_frame_rate) {
|
|
2298
|
-
const parts = videoStream.r_frame_rate.split("/");
|
|
2299
|
-
if (parts.length === 2) {
|
|
2300
|
-
fps = Number(parts[0]) / Number(parts[1]);
|
|
2301
|
-
}
|
|
2302
|
-
else if (parts.length === 1) {
|
|
2303
|
-
fps = Number(parts[0]);
|
|
2304
|
-
}
|
|
2305
|
-
}
|
|
2306
|
-
const duration = Number(info.format?.duration ?? 0);
|
|
2307
|
-
const frameCount = videoStream?.nb_frames
|
|
2308
|
-
? Number(videoStream.nb_frames)
|
|
2309
|
-
: Math.round(duration * fps);
|
|
2310
|
-
return {
|
|
2311
|
-
duration,
|
|
2312
|
-
width: videoStream?.width ?? 0,
|
|
2313
|
-
height: videoStream?.height ?? 0,
|
|
2314
|
-
fps,
|
|
2315
|
-
frame_count: frameCount,
|
|
2316
|
-
codec: videoStream?.codec_name ?? "",
|
|
2317
|
-
has_audio: hasAudio
|
|
2318
|
-
};
|
|
2319
|
-
}
|
|
2320
|
-
catch {
|
|
2321
|
-
return {
|
|
2322
|
-
duration: 0,
|
|
2323
|
-
width: 0,
|
|
2324
|
-
height: 0,
|
|
2325
|
-
fps: 0,
|
|
2326
|
-
frame_count: 0,
|
|
2327
|
-
codec: "",
|
|
2328
|
-
has_audio: false
|
|
2329
|
-
};
|
|
2330
|
-
}
|
|
2331
|
-
finally {
|
|
2332
|
-
await inputFile.cleanup();
|
|
2333
|
-
}
|
|
2334
|
-
}
|
|
2335
|
-
}
|
|
2336
|
-
__decorate([
|
|
2337
|
-
prop({
|
|
2338
|
-
type: "video",
|
|
2339
|
-
default: {
|
|
2340
|
-
type: "video",
|
|
2341
|
-
uri: "",
|
|
2342
|
-
asset_id: null,
|
|
2343
|
-
data: null,
|
|
2344
|
-
metadata: null,
|
|
2345
|
-
duration: null,
|
|
2346
|
-
format: null
|
|
2347
|
-
},
|
|
2348
|
-
title: "Video",
|
|
2349
|
-
description: "The input video to analyze."
|
|
2350
|
-
})
|
|
2351
|
-
], GetVideoInfoNode.prototype, "video", void 0);
|
|
2352
|
-
export const VIDEO_NODES = [
|
|
2353
|
-
TextToVideoNode,
|
|
2354
|
-
ImageToVideoNode,
|
|
2355
|
-
LoadVideoFileNode,
|
|
2356
|
-
SaveVideoFileVideoNode,
|
|
2357
|
-
LoadVideoAssetsNode,
|
|
2358
|
-
SaveVideoNode,
|
|
2359
|
-
FrameIteratorNode,
|
|
2360
|
-
FpsNode,
|
|
2361
|
-
FrameToVideoNode,
|
|
2362
|
-
ConcatVideoNode,
|
|
2363
|
-
TrimVideoNode,
|
|
2364
|
-
ResizeVideoNode,
|
|
2365
|
-
RotateVideoNode,
|
|
2366
|
-
SetSpeedVideoNode,
|
|
2367
|
-
OverlayVideoNode,
|
|
2368
|
-
ColorBalanceVideoNode,
|
|
2369
|
-
DenoiseVideoNode,
|
|
2370
|
-
StabilizeVideoNode,
|
|
2371
|
-
SharpnessVideoNode,
|
|
2372
|
-
BlurVideoNode,
|
|
2373
|
-
SaturationVideoNode,
|
|
2374
|
-
AddSubtitlesVideoNode,
|
|
2375
|
-
ReverseVideoNode,
|
|
2376
|
-
TransitionVideoNode,
|
|
2377
|
-
AddAudioVideoNode,
|
|
2378
|
-
ChromaKeyVideoNode,
|
|
2379
|
-
ExtractAudioVideoNode,
|
|
2380
|
-
ExtractFrameVideoNode,
|
|
2381
|
-
GetVideoInfoNode
|
|
2382
|
-
];
|
|
2383
|
-
//# sourceMappingURL=video.js.map
|