@shaztech/video-pipeline 1.2.0 → 1.3.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.
@@ -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-Dqr1wx5V.js"></script>
13
- <link rel="stylesheet" crossorigin href="./assets/index-Df9WKIy8.css">
12
+ <script type="module" crossorigin src="./assets/index-yrb6jZba.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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shaztech/video-pipeline",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Visual node-based video processing pipeline CLI",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -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.
@@ -45,6 +53,10 @@ function buildDrawtextArgs(srcLabel, {
45
53
  box = false,
46
54
  boxColor = 'black@0.5',
47
55
  padding = 20,
56
+ position = 'bottom-right',
57
+ customX = 0,
58
+ customY = 0,
59
+ startAt = 0,
48
60
  destPath,
49
61
  dryRun = false,
50
62
  }) {
@@ -67,8 +79,7 @@ function buildDrawtextArgs(srcLabel, {
67
79
 
68
80
  const escapedFontFile = escapeFilterPath(fontFile)
69
81
  const escapedTextFile = escapeFilterPath(textFile)
70
- const x = `w-tw-${padding}`
71
- const y = `h-th-${padding}`
82
+ const { x, y } = computePositionXY({ position, padding, customX, customY })
72
83
 
73
84
  let filterStr =
74
85
  `drawtext=fontfile=${escapedFontFile}` +
@@ -82,6 +93,10 @@ function buildDrawtextArgs(srcLabel, {
82
93
  filterStr += `:box=1:boxcolor=${boxColor}:boxborderw=8`
83
94
  }
84
95
 
96
+ if (startAt > 0) {
97
+ filterStr += `:enable=gte(t\\,${startAt})`
98
+ }
99
+
85
100
  return { text, textFile, filterStr }
86
101
  }
87
102
 
@@ -99,15 +114,19 @@ function buildDrawtextArgs(srcLabel, {
99
114
  * @param {string} [opts.fontColor='white']
100
115
  * @param {boolean} [opts.box=false] - draw a semi-transparent background box
101
116
  * @param {string} [opts.boxColor='black@0.5']
102
- * @param {number} [opts.padding=20] - px distance from right & bottom edges
117
+ * @param {number} [opts.padding=20] - px distance from edges (used by presets)
118
+ * @param {string} [opts.position='bottom-right'] - one of the 9 presets or 'custom'
119
+ * @param {number} [opts.customX=0] - x pixel offset (only when position='custom')
120
+ * @param {number} [opts.customY=0] - y pixel offset (only when position='custom')
103
121
  * @param {number} [opts.totalOffset=0] - integer added to the denominator
122
+ * @param {number} [opts.startAt=0] - (video only) seconds before label appears; ignored for images
104
123
  * @param {string} opts.destPath - absolute path to write the annotated image
105
124
  * @param {string} [opts.label] - display label for the run() spinner
106
125
  * @param {boolean} [opts.dryRun=false]
107
126
  */
108
127
  export async function annotateImageWithSequence(srcPath, opts) {
109
128
  const { destPath, label, dryRun = false, index, total } = opts
110
- const { text, textFile, filterStr } = buildDrawtextArgs(srcPath, opts)
129
+ const { text, textFile, filterStr } = buildDrawtextArgs(srcPath, { ...opts, startAt: 0 })
111
130
 
112
131
  console.log(
113
132
  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
- // Skip if whole-video label is active (mutual exclusion enforced at runtime).
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
 
@@ -213,6 +213,9 @@ export async function handleVideoStitcher(node, context, tempRoot, incomingEdges
213
213
  boxColor: sl.boxColor,
214
214
  padding: sl.padding,
215
215
  totalOffset: sl.totalOffset,
216
+ position: sl.position,
217
+ customX: sl.customX,
218
+ customY: sl.customY,
216
219
  destPath,
217
220
  label: `${node.label ?? node.id} [annotate ${runIdx + 1}/${runCount}]`,
218
221
  dryRun: opts.dryRun,
@@ -252,6 +255,10 @@ export async function handleVideoStitcher(node, context, tempRoot, incomingEdges
252
255
  boxColor: videoSl.boxColor,
253
256
  padding: videoSl.padding,
254
257
  totalOffset: videoSl.totalOffset,
258
+ position: videoSl.position,
259
+ customX: videoSl.customX,
260
+ customY: videoSl.customY,
261
+ startAt: videoSl.startAt,
255
262
  destPath: outputFile,
256
263
  label: `${node.label ?? node.id} [label ${runIdx + 1}/${runCount}]`,
257
264
  dryRun: opts.dryRun,
@@ -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) {