@shaztech/video-pipeline 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -4
- package/dist/editor/assets/{index-Df9WKIy8.css → index-CUxLpKni.css} +1 -1
- package/dist/editor/assets/index-DK9i7gno.js +71 -0
- package/dist/editor/index.html +2 -2
- package/package.json +1 -1
- package/src/executor/nodeHandlers/imageAnnotate.js +32 -4
- package/src/executor/nodeHandlers/video-stitcher.js +13 -2
- package/src/server/routes.js +35 -15
- package/src/spec/schema.js +0 -10
- package/dist/editor/assets/index-Dqr1wx5V.js +0 -71
package/dist/editor/index.html
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
html, body, #root { height: 100%; width: 100%; overflow: hidden; }
|
|
10
10
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #1a1a2e; color: #e0e0e0; }
|
|
11
11
|
</style>
|
|
12
|
-
<script type="module" crossorigin src="./assets/index-
|
|
13
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
12
|
+
<script type="module" crossorigin src="./assets/index-DK9i7gno.js"></script>
|
|
13
|
+
<link rel="stylesheet" crossorigin href="./assets/index-CUxLpKni.css">
|
|
14
14
|
</head>
|
|
15
15
|
<body>
|
|
16
16
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -30,6 +30,14 @@ function escapeFilterPath(p) {
|
|
|
30
30
|
return p.replace(/\\/g, '\\\\').replace(/:/g, '\\:').replace(/'/g, "\\'")
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
function computePositionXY({ position = 'bottom-right', padding = 20, customX = 0, customY = 0 }) {
|
|
34
|
+
if (position === 'custom') return { x: String(customX), y: String(customY) }
|
|
35
|
+
const H = { left: String(padding), center: '(w-tw)/2', right: `w-tw-${padding}` }
|
|
36
|
+
const V = { top: String(padding), center: '(h-th)/2', bottom: `h-th-${padding}` }
|
|
37
|
+
const parts = position.split('-')
|
|
38
|
+
return { x: H[parts[1] ?? 'center'], y: V[parts[0]] }
|
|
39
|
+
}
|
|
40
|
+
|
|
33
41
|
/**
|
|
34
42
|
* Validates fontFile and composes the drawtext filter string and sidecar text file path.
|
|
35
43
|
* Shared by both image and video annotation helpers.
|
|
@@ -42,9 +50,15 @@ function buildDrawtextArgs(srcLabel, {
|
|
|
42
50
|
fontFile,
|
|
43
51
|
fontSize = 48,
|
|
44
52
|
fontColor = 'white',
|
|
53
|
+
borderColor,
|
|
54
|
+
borderWidth,
|
|
45
55
|
box = false,
|
|
46
56
|
boxColor = 'black@0.5',
|
|
47
57
|
padding = 20,
|
|
58
|
+
position = 'bottom-right',
|
|
59
|
+
customX = 0,
|
|
60
|
+
customY = 0,
|
|
61
|
+
startAt = 0,
|
|
48
62
|
destPath,
|
|
49
63
|
dryRun = false,
|
|
50
64
|
}) {
|
|
@@ -67,8 +81,7 @@ function buildDrawtextArgs(srcLabel, {
|
|
|
67
81
|
|
|
68
82
|
const escapedFontFile = escapeFilterPath(fontFile)
|
|
69
83
|
const escapedTextFile = escapeFilterPath(textFile)
|
|
70
|
-
const x =
|
|
71
|
-
const y = `h-th-${padding}`
|
|
84
|
+
const { x, y } = computePositionXY({ position, padding, customX, customY })
|
|
72
85
|
|
|
73
86
|
let filterStr =
|
|
74
87
|
`drawtext=fontfile=${escapedFontFile}` +
|
|
@@ -78,10 +91,19 @@ function buildDrawtextArgs(srcLabel, {
|
|
|
78
91
|
`:x=${x}` +
|
|
79
92
|
`:y=${y}`
|
|
80
93
|
|
|
94
|
+
if (borderWidth != null && borderWidth > 0) {
|
|
95
|
+
filterStr += `:borderw=${borderWidth}`
|
|
96
|
+
if (borderColor) filterStr += `:bordercolor=${borderColor}`
|
|
97
|
+
}
|
|
98
|
+
|
|
81
99
|
if (box) {
|
|
82
100
|
filterStr += `:box=1:boxcolor=${boxColor}:boxborderw=8`
|
|
83
101
|
}
|
|
84
102
|
|
|
103
|
+
if (startAt > 0) {
|
|
104
|
+
filterStr += `:enable=gte(t\\,${startAt})`
|
|
105
|
+
}
|
|
106
|
+
|
|
85
107
|
return { text, textFile, filterStr }
|
|
86
108
|
}
|
|
87
109
|
|
|
@@ -97,17 +119,23 @@ function buildDrawtextArgs(srcLabel, {
|
|
|
97
119
|
* @param {string} opts.fontFile - path to a .ttf/.otf/.ttc font file (required)
|
|
98
120
|
* @param {number} [opts.fontSize=48]
|
|
99
121
|
* @param {string} [opts.fontColor='white']
|
|
122
|
+
* @param {string} [opts.borderColor] - color of text stroke/outline (e.g. 'black')
|
|
123
|
+
* @param {number} [opts.borderWidth] - width of text stroke in pixels (0 = off)
|
|
100
124
|
* @param {boolean} [opts.box=false] - draw a semi-transparent background box
|
|
101
125
|
* @param {string} [opts.boxColor='black@0.5']
|
|
102
|
-
* @param {number} [opts.padding=20] - px distance from
|
|
126
|
+
* @param {number} [opts.padding=20] - px distance from edges (used by presets)
|
|
127
|
+
* @param {string} [opts.position='bottom-right'] - one of the 9 presets or 'custom'
|
|
128
|
+
* @param {number} [opts.customX=0] - x pixel offset (only when position='custom')
|
|
129
|
+
* @param {number} [opts.customY=0] - y pixel offset (only when position='custom')
|
|
103
130
|
* @param {number} [opts.totalOffset=0] - integer added to the denominator
|
|
131
|
+
* @param {number} [opts.startAt=0] - (video only) seconds before label appears; ignored for images
|
|
104
132
|
* @param {string} opts.destPath - absolute path to write the annotated image
|
|
105
133
|
* @param {string} [opts.label] - display label for the run() spinner
|
|
106
134
|
* @param {boolean} [opts.dryRun=false]
|
|
107
135
|
*/
|
|
108
136
|
export async function annotateImageWithSequence(srcPath, opts) {
|
|
109
137
|
const { destPath, label, dryRun = false, index, total } = opts
|
|
110
|
-
const { text, textFile, filterStr } = buildDrawtextArgs(srcPath, opts)
|
|
138
|
+
const { text, textFile, filterStr } = buildDrawtextArgs(srcPath, { ...opts, startAt: 0 })
|
|
111
139
|
|
|
112
140
|
console.log(
|
|
113
141
|
chalk.dim(` [seq-label] Annotating ${path.basename(srcPath)} `) +
|
|
@@ -188,10 +188,10 @@ export async function handleVideoStitcher(node, context, tempRoot, incomingEdges
|
|
|
188
188
|
if (videoSl?.enabled && !opts.dryRun) mkdirSync(path.dirname(stitchTarget), { recursive: true })
|
|
189
189
|
|
|
190
190
|
// Pre-process any fixed image inputs that have sequenceLabel.enabled.
|
|
191
|
-
//
|
|
191
|
+
// Per-image and whole-video labels can both be active — per-image annotations
|
|
192
|
+
// get baked into frames and the whole-video pass then draws on top.
|
|
192
193
|
const resolvedInputs = await Promise.all(
|
|
193
194
|
inputs.map(async (input) => {
|
|
194
|
-
if (videoSl?.enabled) return input
|
|
195
195
|
const sl = input.sequenceLabel
|
|
196
196
|
if (!sl?.enabled || !isImage(input.value)) return input
|
|
197
197
|
|
|
@@ -209,10 +209,15 @@ export async function handleVideoStitcher(node, context, tempRoot, incomingEdges
|
|
|
209
209
|
fontFile: sl.fontFile,
|
|
210
210
|
fontSize: sl.fontSize,
|
|
211
211
|
fontColor: sl.fontColor,
|
|
212
|
+
borderColor: sl.borderColor,
|
|
213
|
+
borderWidth: sl.borderWidth,
|
|
212
214
|
box: sl.box,
|
|
213
215
|
boxColor: sl.boxColor,
|
|
214
216
|
padding: sl.padding,
|
|
215
217
|
totalOffset: sl.totalOffset,
|
|
218
|
+
position: sl.position,
|
|
219
|
+
customX: sl.customX,
|
|
220
|
+
customY: sl.customY,
|
|
216
221
|
destPath,
|
|
217
222
|
label: `${node.label ?? node.id} [annotate ${runIdx + 1}/${runCount}]`,
|
|
218
223
|
dryRun: opts.dryRun,
|
|
@@ -248,10 +253,16 @@ export async function handleVideoStitcher(node, context, tempRoot, incomingEdges
|
|
|
248
253
|
fontFile: videoSl.fontFile,
|
|
249
254
|
fontSize: videoSl.fontSize,
|
|
250
255
|
fontColor: videoSl.fontColor,
|
|
256
|
+
borderColor: videoSl.borderColor,
|
|
257
|
+
borderWidth: videoSl.borderWidth,
|
|
251
258
|
box: videoSl.box,
|
|
252
259
|
boxColor: videoSl.boxColor,
|
|
253
260
|
padding: videoSl.padding,
|
|
254
261
|
totalOffset: videoSl.totalOffset,
|
|
262
|
+
position: videoSl.position,
|
|
263
|
+
customX: videoSl.customX,
|
|
264
|
+
customY: videoSl.customY,
|
|
265
|
+
startAt: videoSl.startAt,
|
|
255
266
|
destPath: outputFile,
|
|
256
267
|
label: `${node.label ?? node.id} [label ${runIdx + 1}/${runCount}]`,
|
|
257
268
|
dryRun: opts.dryRun,
|
package/src/server/routes.js
CHANGED
|
@@ -14,27 +14,40 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { readFileSync, writeFileSync, renameSync } from 'fs'
|
|
17
|
+
import { readFileSync, writeFileSync, renameSync, existsSync, statSync } from 'fs'
|
|
18
18
|
import { execFile } from 'child_process'
|
|
19
19
|
import path from 'path'
|
|
20
20
|
import { Router } from 'express'
|
|
21
21
|
|
|
22
|
-
function
|
|
22
|
+
function resolveStartFolder(from) {
|
|
23
|
+
if (!from || typeof from !== 'string') return null
|
|
24
|
+
try {
|
|
25
|
+
if (!existsSync(from)) return null
|
|
26
|
+
return statSync(from).isDirectory() ? from : path.dirname(from)
|
|
27
|
+
} catch { return null }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function nativeBrowse(from) {
|
|
31
|
+
const start = resolveStartFolder(from)
|
|
23
32
|
return new Promise((resolve) => {
|
|
24
33
|
const { platform } = process
|
|
25
34
|
if (platform === 'darwin') {
|
|
26
|
-
|
|
35
|
+
const loc = start ? ` default location (POSIX file "${start.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}")` : ''
|
|
36
|
+
execFile('osascript', ['-e', `POSIX path of (choose file${loc})`], (err, stdout) => {
|
|
27
37
|
resolve(err ? null : stdout.trim())
|
|
28
38
|
})
|
|
29
39
|
} else if (platform === 'linux') {
|
|
30
|
-
|
|
40
|
+
const args = ['--file-selection']
|
|
41
|
+
if (start) args.push(`--filename=${start.endsWith('/') ? start : start + '/'}`)
|
|
42
|
+
execFile('zenity', args, (err, stdout) => {
|
|
31
43
|
if (!err) return resolve(stdout.trim())
|
|
32
|
-
execFile('kdialog', ['--getopenfilename', '.'], (err2, stdout2) => {
|
|
44
|
+
execFile('kdialog', ['--getopenfilename', start || '.'], (err2, stdout2) => {
|
|
33
45
|
resolve(err2 ? null : stdout2.trim())
|
|
34
46
|
})
|
|
35
47
|
})
|
|
36
48
|
} else if (platform === 'win32') {
|
|
37
|
-
const
|
|
49
|
+
const initLine = start ? `$d.InitialDirectory = '${start.replace(/'/g, "''")}'; ` : ''
|
|
50
|
+
const ps = `Add-Type -AssemblyName System.Windows.Forms; $d = New-Object System.Windows.Forms.OpenFileDialog; ${initLine}if ($d.ShowDialog() -eq 'OK') { $d.FileName }`
|
|
38
51
|
execFile('powershell', ['-Command', ps], (err, stdout) => {
|
|
39
52
|
resolve(err ? null : stdout.trim())
|
|
40
53
|
})
|
|
@@ -44,22 +57,27 @@ function nativeBrowse() {
|
|
|
44
57
|
})
|
|
45
58
|
}
|
|
46
59
|
|
|
47
|
-
function nativeBrowseFolder() {
|
|
60
|
+
function nativeBrowseFolder(from) {
|
|
61
|
+
const start = resolveStartFolder(from)
|
|
48
62
|
return new Promise((resolve) => {
|
|
49
63
|
const { platform } = process
|
|
50
64
|
if (platform === 'darwin') {
|
|
51
|
-
|
|
65
|
+
const loc = start ? ` default location (POSIX file "${start.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}")` : ''
|
|
66
|
+
execFile('osascript', ['-e', `POSIX path of (choose folder${loc})`], (err, stdout) => {
|
|
52
67
|
resolve(err ? null : stdout.trim())
|
|
53
68
|
})
|
|
54
69
|
} else if (platform === 'linux') {
|
|
55
|
-
|
|
70
|
+
const args = ['--file-selection', '--directory']
|
|
71
|
+
if (start) args.push(`--filename=${start.endsWith('/') ? start : start + '/'}`)
|
|
72
|
+
execFile('zenity', args, (err, stdout) => {
|
|
56
73
|
if (!err) return resolve(stdout.trim())
|
|
57
|
-
execFile('kdialog', ['--getexistingdirectory', '.'], (err2, stdout2) => {
|
|
74
|
+
execFile('kdialog', ['--getexistingdirectory', start || '.'], (err2, stdout2) => {
|
|
58
75
|
resolve(err2 ? null : stdout2.trim())
|
|
59
76
|
})
|
|
60
77
|
})
|
|
61
78
|
} else if (platform === 'win32') {
|
|
62
|
-
const
|
|
79
|
+
const initLine = start ? `$d.SelectedPath = '${start.replace(/'/g, "''")}'; ` : ''
|
|
80
|
+
const ps = `Add-Type -AssemblyName System.Windows.Forms; $d = New-Object System.Windows.Forms.FolderBrowserDialog; ${initLine}if ($d.ShowDialog() -eq 'OK') { $d.SelectedPath }`
|
|
63
81
|
execFile('powershell', ['-Command', ps], (err, stdout) => {
|
|
64
82
|
resolve(err ? null : stdout.trim())
|
|
65
83
|
})
|
|
@@ -89,14 +107,16 @@ export function createRoutes(specPath, broadcast) {
|
|
|
89
107
|
})
|
|
90
108
|
|
|
91
109
|
// GET /api/browse — open a native OS file picker, return full path
|
|
92
|
-
|
|
93
|
-
|
|
110
|
+
// Optional ?from=<path> opens the picker at that path's parent folder
|
|
111
|
+
router.get('/api/browse', async (req, res) => {
|
|
112
|
+
const filePath = await nativeBrowse(typeof req.query.from === 'string' ? req.query.from : null)
|
|
94
113
|
res.json({ path: filePath })
|
|
95
114
|
})
|
|
96
115
|
|
|
97
116
|
// GET /api/browse-folder — open a native OS folder picker, return full path
|
|
98
|
-
|
|
99
|
-
|
|
117
|
+
// Optional ?from=<path> opens the picker at that path (or its parent if a file)
|
|
118
|
+
router.get('/api/browse-folder', async (req, res) => {
|
|
119
|
+
const folderPath = await nativeBrowseFolder(typeof req.query.from === 'string' ? req.query.from : null)
|
|
100
120
|
res.json({ path: folderPath })
|
|
101
121
|
})
|
|
102
122
|
|
package/src/spec/schema.js
CHANGED
|
@@ -64,16 +64,6 @@ export function validateSpec(spec) {
|
|
|
64
64
|
if (!node.config || typeof node.config !== 'object') {
|
|
65
65
|
errors.push(`Node "${node.id}" missing config object`)
|
|
66
66
|
}
|
|
67
|
-
|
|
68
|
-
if (node.type === 'video-stitcher' && node.config) {
|
|
69
|
-
const videoSlEnabled = node.config.sequenceLabel?.enabled === true
|
|
70
|
-
const imageSlEnabled = (node.config.inputOrder ?? []).some((i) => i.sequenceLabel?.enabled === true)
|
|
71
|
-
if (videoSlEnabled && imageSlEnabled) {
|
|
72
|
-
errors.push(
|
|
73
|
-
`Node "${node.id}" (video-stitcher): config.sequenceLabel.enabled and per-image sequenceLabel.enabled are mutually exclusive`
|
|
74
|
-
)
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
67
|
}
|
|
78
68
|
|
|
79
69
|
for (const edge of spec.edges) {
|