@mevdragon/vidfarm-devcli 0.2.2 → 0.2.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mevdragon/vidfarm-devcli",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Developer CLI for running the Vidfarm local template platform.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,7 +21,7 @@
21
21
  },
22
22
  "repository": {
23
23
  "type": "git",
24
- "url": "git+https://github.com/OfficeXApp/vidfarm-devcli.git"
24
+ "url": "git+https://github.com/OfficeXApp/vidfarm.git"
25
25
  },
26
26
  "engines": {
27
27
  "node": ">=22.0.0"
@@ -5,6 +5,7 @@ Standalone GitHub-distributable package for the Vidfarm `template_0000` starter
5
5
  This package exists to give third-party developers a concrete example of the Vidfarm template contract.
6
6
 
7
7
  Developers should treat this repo as an authoring and review unit.
8
+ The canonical template implementation lives in `src/template.ts`; the build emits `src/template.js` for runtime loading.
8
9
 
9
10
  When a template is ready, it is handed off for Vidfarm admin review and approval before it becomes available on the hosted platform.
10
11
 
@@ -19,7 +20,7 @@ Production integration settings are checked into `template.config.json`. AI agen
19
20
  - project identity
20
21
  - `template_id` as the self-issued UUIDv4 platform identifier
21
22
  - `slug_id` as the human-readable stable slug
22
- - the template module path
23
+ - the template module path, which should be the TypeScript source entrypoint `src/template.ts`
23
24
  - the local Remotion composition settings used by the starter template
24
25
 
25
26
  ## Included
@@ -18,12 +18,44 @@ Behavior:
18
18
 
19
19
  - `create_slideshow` generates exact 9:16 images, preserves the exact overlay text, and outputs finished slide frames plus a manifest
20
20
  - `render_video` turns explicit finished slide frames into an auto-advancing vertical MP4
21
+ - when chaining operations, pass `result.renderVideoInput` from `create_slideshow` into `render_video` without re-encoding or HTML-escaping the slide URLs
21
22
  - reserves space for TikTok UI chrome at the top, right edge, and bottom caption area
22
23
  - keeps subjects away from edge crops and avoids cutting off faces, pets, hands, and products
23
24
  - prefers centered native-caption placement using image analysis first, with local fallback scoring
24
25
  - never uses a full paragraph wash behind captions; when enabled, text backgrounds render as tight chips around the words
25
26
  - restricts caption fonts and text background colors to the checked-in starter option lists in `src/style-options.ts`
26
27
 
28
+ AI dependency contract for agents:
29
+
30
+ - the current Vidfarm template standard does not have a first-class `dependencies` field, so this section is the authoritative dependency declaration for this template
31
+ - `defaultProvider`, `textModel`, and `imageModel` are config values, not a validated provider-model matrix
32
+ - agents should keep provider and model choices inside the combinations listed below unless they are intentionally extending runtime support
33
+
34
+ Documented provider-model combinations for this template:
35
+
36
+ - `openai`
37
+ models:
38
+ `image`: `gpt-image-1`, `gpt-image-2`
39
+ `text` and `layout_analysis`: `gpt-5.4`
40
+ planned but not wired here: `sora-2`
41
+ - `gemini`
42
+ models:
43
+ `image`: `gemini-3.1-flash-image-preview`, `gemini-2.5-flash-image`
44
+ `text` and `layout_analysis`: `gemini-3.1-flash-lite`, `gemini-2.5-flash-lite`
45
+ planned but not wired here: `veo-3.0-generate-001`
46
+ - `openrouter`
47
+ models:
48
+ `text` and `layout_analysis`: `qwen/qwen3.6-flash`
49
+ aspirational image models: `bytedance/seedance-2.0`, `bytedance-seed/seedream-4.5`
50
+
51
+ Runtime caveats:
52
+
53
+ - `openrouter` text and layout-analysis calls work through the OpenRouter chat-completions path
54
+ - `openrouter` image generation is not a first-class adapter path in the current runtime, so do not assume `seedance-2.0` or `seedream-4.5` will work end to end without provider-runtime changes
55
+ - OpenAI image generation in the current runtime does not support `image_prompt_attachments`; Gemini is the safest path when reference-image attachments matter
56
+ - OpenAI image generation does not expose an exact native `9:16` output size in the current API. For `template_0000`, use portrait output plus explicit `9:16` framing instructions, then preserve composition with contain-plus-blur normalization instead of crop-zooming.
57
+ - this template does not expose a video-generation operation, so `sora-2` and `veo-3.0-generate-001` are documented here for future dependency planning only
58
+
27
59
  Allowed caption fonts:
28
60
 
29
61
  - `source_code_pro`
@@ -4,25 +4,60 @@ export async function normalizeToPortraitFrame(input, target = { width: 1080, he
4
4
  const oriented = sharp(buffer, { density: 144 }).rotate();
5
5
  const trimmed = await trimFlatBorders(oriented);
6
6
  const targetAspect = target.width / target.height;
7
- // If the model already returned a near-native vertical frame, avoid the
8
- // activity crop pass because it can misread low-detail edges as padding.
9
7
  const shouldPreserveFraming = await isCloseToAspect(trimmed, targetAspect, 0.025);
10
8
  const cropped = shouldPreserveFraming ? trimmed : await cropToActiveImageRegion(trimmed);
11
- const portraitCrop = await cropToPortraitAspect(cropped, targetAspect);
12
- const output = await portraitCrop
9
+ const normalized = await resizeWithBlurredContain(cropped, target, targetAspect);
10
+ return {
11
+ bytes: normalized,
12
+ contentType: "image/png",
13
+ width: target.width,
14
+ height: target.height
15
+ };
16
+ }
17
+ async function resizeWithBlurredContain(image, target, targetAspect) {
18
+ const metadata = await image.metadata();
19
+ const width = metadata.width ?? 0;
20
+ const height = metadata.height ?? 0;
21
+ if (!width || !height) {
22
+ return image
23
+ .resize(target.width, target.height, {
24
+ fit: "cover",
25
+ position: sharp.strategy.attention
26
+ })
27
+ .png()
28
+ .toBuffer();
29
+ }
30
+ const aspect = width / height;
31
+ if (Math.abs(aspect - targetAspect) <= 0.015) {
32
+ return image
33
+ .resize(target.width, target.height, {
34
+ fit: "fill"
35
+ })
36
+ .png()
37
+ .toBuffer();
38
+ }
39
+ const foreground = await image
40
+ .clone()
41
+ .resize(target.width, target.height, {
42
+ fit: "contain",
43
+ background: { r: 0, g: 0, b: 0, alpha: 0 }
44
+ })
45
+ .png()
46
+ .toBuffer();
47
+ const background = await image
48
+ .clone()
13
49
  .resize(target.width, target.height, {
14
50
  fit: "cover",
15
51
  position: sharp.strategy.attention
16
52
  })
53
+ .blur(20)
54
+ .modulate({ brightness: 0.92, saturation: 0.9 })
55
+ .png()
56
+ .toBuffer();
57
+ return sharp(background)
58
+ .composite([{ input: foreground }])
17
59
  .png()
18
60
  .toBuffer();
19
- const exact = await ensureExactPixelSize(output, target);
20
- return {
21
- bytes: exact,
22
- contentType: "image/png",
23
- width: target.width,
24
- height: target.height
25
- };
26
61
  }
27
62
  async function isCloseToAspect(image, targetAspect, tolerance) {
28
63
  const metadata = await image.metadata();
@@ -77,27 +112,6 @@ async function cropToActiveImageRegion(image) {
77
112
  const height = Math.min(sourceHeight - top, Math.max(1, Math.ceil((bounds.height / sampleHeight) * sourceHeight)));
78
113
  return image.extract({ left, top, width, height });
79
114
  }
80
- async function cropToPortraitAspect(image, targetAspect) {
81
- const metadata = await image.metadata();
82
- const sourceWidth = metadata.width ?? 0;
83
- const sourceHeight = metadata.height ?? 0;
84
- if (!sourceWidth || !sourceHeight) {
85
- return image;
86
- }
87
- const sourceAspect = sourceWidth / sourceHeight;
88
- if (Math.abs(sourceAspect - targetAspect) < 0.015) {
89
- return image;
90
- }
91
- const focus = await detectFocusPoint(image, sourceWidth, sourceHeight);
92
- if (sourceAspect > targetAspect) {
93
- const cropWidth = Math.max(1, Math.min(sourceWidth, Math.round(sourceHeight * targetAspect)));
94
- const left = clamp(Math.round(focus.x - cropWidth / 2), 0, sourceWidth - cropWidth);
95
- return image.extract({ left, top: 0, width: cropWidth, height: sourceHeight });
96
- }
97
- const cropHeight = Math.max(1, Math.min(sourceHeight, Math.round(sourceWidth / targetAspect)));
98
- const top = clamp(Math.round(focus.y - cropHeight / 2), 0, sourceHeight - cropHeight);
99
- return image.extract({ left: 0, top, width: sourceWidth, height: cropHeight });
100
- }
101
115
  function detectActiveBounds(sample, width, height) {
102
116
  const rowActivity = new Array(height).fill(0);
103
117
  const colActivity = new Array(width).fill(0);
@@ -142,46 +156,6 @@ function detectActiveBounds(sample, width, height) {
142
156
  height: croppedHeight
143
157
  };
144
158
  }
145
- async function detectFocusPoint(image, sourceWidth, sourceHeight) {
146
- const sampleWidth = 120;
147
- const sampleHeight = Math.max(1, Math.round((sourceHeight / Math.max(sourceWidth, 1)) * sampleWidth));
148
- const sample = await image
149
- .clone()
150
- .resize(sampleWidth, sampleHeight, { fit: "fill" })
151
- .grayscale()
152
- .raw()
153
- .toBuffer();
154
- let weightedX = 0;
155
- let weightedY = 0;
156
- let totalWeight = 0;
157
- for (let y = 0; y < sampleHeight; y += 1) {
158
- for (let x = 0; x < sampleWidth; x += 1) {
159
- const index = y * sampleWidth + x;
160
- const current = sample[index] ?? 0;
161
- const left = x > 0 ? sample[index - 1] ?? current : current;
162
- const up = y > 0 ? sample[index - sampleWidth] ?? current : current;
163
- const right = x < sampleWidth - 1 ? sample[index + 1] ?? current : current;
164
- const down = y < sampleHeight - 1 ? sample[index + sampleWidth] ?? current : current;
165
- const energy = Math.abs(current - left) +
166
- Math.abs(current - right) +
167
- Math.abs(current - up) +
168
- Math.abs(current - down);
169
- const centerBiasX = 1 - Math.abs((x + 0.5) / sampleWidth - 0.5) * 0.45;
170
- const centerBiasY = 1 - Math.abs((y + 0.5) / sampleHeight - 0.5) * 0.35;
171
- const weight = Math.max(energy, 1) * centerBiasX * centerBiasY;
172
- weightedX += (x + 0.5) * weight;
173
- weightedY += (y + 0.5) * weight;
174
- totalWeight += weight;
175
- }
176
- }
177
- if (totalWeight <= 0) {
178
- return { x: sourceWidth / 2, y: sourceHeight / 2 };
179
- }
180
- return {
181
- x: (weightedX / totalWeight / sampleWidth) * sourceWidth,
182
- y: (weightedY / totalWeight / sampleHeight) * sourceHeight
183
- };
184
- }
185
159
  function findActiveRange(values, minSpan) {
186
160
  const mean = values.reduce((sum, value) => sum + value, 0) / Math.max(values.length, 1);
187
161
  const max = values.reduce((best, value) => Math.max(best, value), 0);
@@ -226,17 +200,3 @@ function smoothSeries(values, radius) {
226
200
  function clamp(value, min, max) {
227
201
  return Math.min(Math.max(value, min), max);
228
202
  }
229
- async function ensureExactPixelSize(input, target) {
230
- const metadata = await sharp(input).metadata();
231
- if (metadata.width === target.width && metadata.height === target.height) {
232
- return input;
233
- }
234
- const exactCrop = await cropToPortraitAspect(sharp(input), target.width / target.height);
235
- return exactCrop
236
- .resize(target.width, target.height, {
237
- fit: "cover",
238
- position: sharp.strategy.attention
239
- })
240
- .png()
241
- .toBuffer();
242
- }
@@ -8,26 +8,70 @@ export async function normalizeToPortraitFrame(
8
8
  const oriented = sharp(buffer, { density: 144 }).rotate();
9
9
  const trimmed = await trimFlatBorders(oriented);
10
10
  const targetAspect = target.width / target.height;
11
- // If the model already returned a near-native vertical frame, avoid the
12
- // activity crop pass because it can misread low-detail edges as padding.
13
11
  const shouldPreserveFraming = await isCloseToAspect(trimmed, targetAspect, 0.025);
14
12
  const cropped = shouldPreserveFraming ? trimmed : await cropToActiveImageRegion(trimmed);
15
- const portraitCrop = await cropToPortraitAspect(cropped, targetAspect);
16
- const output = await portraitCrop
13
+ const normalized = await resizeWithBlurredContain(cropped, target, targetAspect);
14
+
15
+ return {
16
+ bytes: normalized,
17
+ contentType: "image/png" as const,
18
+ width: target.width,
19
+ height: target.height
20
+ };
21
+ }
22
+
23
+ async function resizeWithBlurredContain(
24
+ image: sharp.Sharp,
25
+ target: { width: number; height: number },
26
+ targetAspect: number
27
+ ) {
28
+ const metadata = await image.metadata();
29
+ const width = metadata.width ?? 0;
30
+ const height = metadata.height ?? 0;
31
+ if (!width || !height) {
32
+ return image
33
+ .resize(target.width, target.height, {
34
+ fit: "cover",
35
+ position: sharp.strategy.attention
36
+ })
37
+ .png()
38
+ .toBuffer();
39
+ }
40
+
41
+ const aspect = width / height;
42
+ if (Math.abs(aspect - targetAspect) <= 0.015) {
43
+ return image
44
+ .resize(target.width, target.height, {
45
+ fit: "fill"
46
+ })
47
+ .png()
48
+ .toBuffer();
49
+ }
50
+
51
+ const foreground = await image
52
+ .clone()
53
+ .resize(target.width, target.height, {
54
+ fit: "contain",
55
+ background: { r: 0, g: 0, b: 0, alpha: 0 }
56
+ })
57
+ .png()
58
+ .toBuffer();
59
+
60
+ const background = await image
61
+ .clone()
17
62
  .resize(target.width, target.height, {
18
63
  fit: "cover",
19
64
  position: sharp.strategy.attention
20
65
  })
66
+ .blur(20)
67
+ .modulate({ brightness: 0.92, saturation: 0.9 })
21
68
  .png()
22
69
  .toBuffer();
23
- const exact = await ensureExactPixelSize(output, target);
24
70
 
25
- return {
26
- bytes: exact,
27
- contentType: "image/png" as const,
28
- width: target.width,
29
- height: target.height
30
- };
71
+ return sharp(background)
72
+ .composite([{ input: foreground }])
73
+ .png()
74
+ .toBuffer();
31
75
  }
32
76
 
33
77
  async function isCloseToAspect(image: sharp.Sharp, targetAspect: number, tolerance: number) {
@@ -93,31 +137,6 @@ async function cropToActiveImageRegion(image: sharp.Sharp) {
93
137
  return image.extract({ left, top, width, height });
94
138
  }
95
139
 
96
- async function cropToPortraitAspect(image: sharp.Sharp, targetAspect: number) {
97
- const metadata = await image.metadata();
98
- const sourceWidth = metadata.width ?? 0;
99
- const sourceHeight = metadata.height ?? 0;
100
- if (!sourceWidth || !sourceHeight) {
101
- return image;
102
- }
103
-
104
- const sourceAspect = sourceWidth / sourceHeight;
105
- if (Math.abs(sourceAspect - targetAspect) < 0.015) {
106
- return image;
107
- }
108
-
109
- const focus = await detectFocusPoint(image, sourceWidth, sourceHeight);
110
- if (sourceAspect > targetAspect) {
111
- const cropWidth = Math.max(1, Math.min(sourceWidth, Math.round(sourceHeight * targetAspect)));
112
- const left = clamp(Math.round(focus.x - cropWidth / 2), 0, sourceWidth - cropWidth);
113
- return image.extract({ left, top: 0, width: cropWidth, height: sourceHeight });
114
- }
115
-
116
- const cropHeight = Math.max(1, Math.min(sourceHeight, Math.round(sourceWidth / targetAspect)));
117
- const top = clamp(Math.round(focus.y - cropHeight / 2), 0, sourceHeight - cropHeight);
118
- return image.extract({ left: 0, top, width: sourceWidth, height: cropHeight });
119
- }
120
-
121
140
  function detectActiveBounds(sample: Buffer, width: number, height: number) {
122
141
  const rowActivity = new Array<number>(height).fill(0);
123
142
  const colActivity = new Array<number>(width).fill(0);
@@ -170,52 +189,6 @@ function detectActiveBounds(sample: Buffer, width: number, height: number) {
170
189
  };
171
190
  }
172
191
 
173
- async function detectFocusPoint(image: sharp.Sharp, sourceWidth: number, sourceHeight: number) {
174
- const sampleWidth = 120;
175
- const sampleHeight = Math.max(1, Math.round((sourceHeight / Math.max(sourceWidth, 1)) * sampleWidth));
176
- const sample = await image
177
- .clone()
178
- .resize(sampleWidth, sampleHeight, { fit: "fill" })
179
- .grayscale()
180
- .raw()
181
- .toBuffer();
182
-
183
- let weightedX = 0;
184
- let weightedY = 0;
185
- let totalWeight = 0;
186
-
187
- for (let y = 0; y < sampleHeight; y += 1) {
188
- for (let x = 0; x < sampleWidth; x += 1) {
189
- const index = y * sampleWidth + x;
190
- const current = sample[index] ?? 0;
191
- const left = x > 0 ? sample[index - 1] ?? current : current;
192
- const up = y > 0 ? sample[index - sampleWidth] ?? current : current;
193
- const right = x < sampleWidth - 1 ? sample[index + 1] ?? current : current;
194
- const down = y < sampleHeight - 1 ? sample[index + sampleWidth] ?? current : current;
195
- const energy =
196
- Math.abs(current - left) +
197
- Math.abs(current - right) +
198
- Math.abs(current - up) +
199
- Math.abs(current - down);
200
- const centerBiasX = 1 - Math.abs((x + 0.5) / sampleWidth - 0.5) * 0.45;
201
- const centerBiasY = 1 - Math.abs((y + 0.5) / sampleHeight - 0.5) * 0.35;
202
- const weight = Math.max(energy, 1) * centerBiasX * centerBiasY;
203
- weightedX += (x + 0.5) * weight;
204
- weightedY += (y + 0.5) * weight;
205
- totalWeight += weight;
206
- }
207
- }
208
-
209
- if (totalWeight <= 0) {
210
- return { x: sourceWidth / 2, y: sourceHeight / 2 };
211
- }
212
-
213
- return {
214
- x: (weightedX / totalWeight / sampleWidth) * sourceWidth,
215
- y: (weightedY / totalWeight / sampleHeight) * sourceHeight
216
- };
217
- }
218
-
219
192
  function findActiveRange(values: number[], minSpan: number) {
220
193
  const mean = values.reduce((sum, value) => sum + value, 0) / Math.max(values.length, 1);
221
194
  const max = values.reduce((best, value) => Math.max(best, value), 0);
@@ -266,19 +239,3 @@ function smoothSeries(values: number[], radius: number) {
266
239
  function clamp(value: number, min: number, max: number) {
267
240
  return Math.min(Math.max(value, min), max);
268
241
  }
269
-
270
- async function ensureExactPixelSize(input: Buffer, target: { width: number; height: number }) {
271
- const metadata = await sharp(input).metadata();
272
- if (metadata.width === target.width && metadata.height === target.height) {
273
- return input;
274
- }
275
-
276
- const exactCrop = await cropToPortraitAspect(sharp(input), target.width / target.height);
277
- return exactCrop
278
- .resize(target.width, target.height, {
279
- fit: "cover",
280
- position: sharp.strategy.attention
281
- })
282
- .png()
283
- .toBuffer();
284
- }
@@ -0,0 +1,9 @@
1
+ // Generated by `vidfarm analyze-viral-dna` and `vidfarm analyze-visual-dna`.
2
+ // Keep source notes in `research/source_notes.md` and reference media in `research/preview/`.
3
+ export const templateLinkToOriginal = "";
4
+ export const templateSourceNotesPath = "research/source_notes.md";
5
+ export const templatePreviewMediaRelativePaths = [];
6
+ export const templateViralDna = "Run `vidfarm analyze-viral-dna --template-dir .` after adding source notes and preview media.";
7
+ export const templateVisualDna = "Run `vidfarm analyze-visual-dna --template-dir .` after adding source notes and preview media.";
8
+ export const templateViralDnaAnalysis = null;
9
+ export const templateVisualDnaAnalysis = null;