@mevdragon/vidfarm-devcli 0.20.6 → 0.20.9

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.
Files changed (157) hide show
  1. package/.agents/skills/editor-capabilities/SKILL.md +15 -1
  2. package/.agents/skills/vidfarm-director/SKILL.md +106 -0
  3. package/.agents/skills/vidfarm-director/recipes/find-and-fork-template.md +16 -0
  4. package/.agents/skills/vidfarm-director/recipes/local-edit-render-approve.md +13 -0
  5. package/.agents/skills/vidfarm-director/recipes/onboard-a-new-director.md +13 -0
  6. package/.agents/skills/vidfarm-director/recipes/retheme-template.md +17 -0
  7. package/.agents/skills/vidfarm-director/references/assets-and-sourcing.md +117 -0
  8. package/.agents/skills/vidfarm-director/references/automation-and-local-dev.md +245 -0
  9. package/.agents/skills/vidfarm-director/references/core-workflows.md +273 -0
  10. package/.agents/skills/vidfarm-director/references/editor-workflows.md +365 -0
  11. package/.agents/skills/vidfarm-director/references/onboarding.md +28 -0
  12. package/.agents/skills/vidfarm-director/references/primitives.md +308 -0
  13. package/SKILL.director.md +517 -250
  14. package/dist/src/cli.js +563 -99
  15. package/dist/src/devcli/clips.js +29 -12
  16. package/dist/src/devcli/doctor.js +2 -2
  17. package/dist/src/devcli/local-backend.js +46 -2
  18. package/dist/src/services/clip-curation/cost.js +5 -1
  19. package/dist/src/services/clip-curation/gemini.js +14 -2
  20. package/dist/src/services/clip-curation/hunt.js +56 -0
  21. package/dist/src/services/clip-curation/index.js +1 -1
  22. package/dist/src/services/clip-curation/scan.js +5 -3
  23. package/package.json +30 -10
  24. package/SKILL.platform.md +0 -432
  25. package/demo/README.md +0 -28
  26. package/demo/dist/app.css +0 -1
  27. package/demo/dist/app.js +0 -1850
  28. package/demo/dist/chunks/chunk-DXB73IDG.js +0 -1
  29. package/demo/dist/chunks/chunk-S7OWAJDS.js +0 -36
  30. package/demo/dist/chunks/chunk-VTIBZ6AN.js +0 -1
  31. package/demo/dist/chunks/dist-ADSJKBVE.js +0 -332
  32. package/demo/dist/chunks/domEditingLayers-VZMLL4AP-SGHWPND4.js +0 -1
  33. package/demo/dist/chunks/hyperframes-player-XB65TCD6.js +0 -425
  34. package/demo/dist/chunks/lib-XAQ37YOE.js +0 -1
  35. package/demo/dist/chunks/src-TJ2QYA4U.js +0 -207
  36. package/demo/dist/favicon.ico +0 -0
  37. package/demo/dist/icons/timeline/audio.svg +0 -7
  38. package/demo/dist/icons/timeline/captions.svg +0 -5
  39. package/demo/dist/icons/timeline/composition.svg +0 -12
  40. package/demo/dist/icons/timeline/image.svg +0 -18
  41. package/demo/dist/icons/timeline/music.svg +0 -10
  42. package/demo/dist/icons/timeline/text.svg +0 -3
  43. package/demo/dist/index.html +0 -15
  44. package/dist/src/account-pages-legacy.js +0 -9396
  45. package/dist/src/account-pages.js +0 -61
  46. package/dist/src/app.js +0 -21603
  47. package/dist/src/composition-runtime.js +0 -1053
  48. package/dist/src/config.js +0 -217
  49. package/dist/src/context.js +0 -447
  50. package/dist/src/dev-app-legacy.js +0 -739
  51. package/dist/src/dev-app.js +0 -6
  52. package/dist/src/devcli/migrate-local.js +0 -140
  53. package/dist/src/devcli/sync.js +0 -368
  54. package/dist/src/domain.js +0 -5
  55. package/dist/src/editor-chat-history.js +0 -82
  56. package/dist/src/editor-chat.js +0 -828
  57. package/dist/src/editor-dark-theme.js +0 -1128
  58. package/dist/src/frontend/debug.js +0 -71
  59. package/dist/src/frontend/discover-client.js +0 -130
  60. package/dist/src/frontend/discover-store.js +0 -23
  61. package/dist/src/frontend/file-directory.js +0 -1018
  62. package/dist/src/frontend/flockposter-cache-store.js +0 -124
  63. package/dist/src/frontend/homepage-client.js +0 -446
  64. package/dist/src/frontend/homepage-shared.js +0 -201
  65. package/dist/src/frontend/homepage-store.js +0 -66
  66. package/dist/src/frontend/homepage-view.js +0 -705
  67. package/dist/src/frontend/page-runtime-client.js +0 -132
  68. package/dist/src/frontend/page-runtime-store.js +0 -9
  69. package/dist/src/frontend/sentry.js +0 -42
  70. package/dist/src/frontend/template-editor-chat.js +0 -4181
  71. package/dist/src/help-page.js +0 -346
  72. package/dist/src/homepage.js +0 -1458
  73. package/dist/src/index.js +0 -16
  74. package/dist/src/instrument.js +0 -30
  75. package/dist/src/landing-page.js +0 -384
  76. package/dist/src/page-runtime.js +0 -2
  77. package/dist/src/page-shell.js +0 -1452
  78. package/dist/src/primitive-context.js +0 -416
  79. package/dist/src/primitive-registry.js +0 -3940
  80. package/dist/src/primitive-sdk.js +0 -4
  81. package/dist/src/primitives/hyperframes-media.js +0 -108
  82. package/dist/src/react-page-shell.js +0 -35
  83. package/dist/src/ready-post-schedule-component.js +0 -1540
  84. package/dist/src/registry.js +0 -296
  85. package/dist/src/reskin/agency-page.js +0 -299
  86. package/dist/src/reskin/calendar-page.js +0 -568
  87. package/dist/src/reskin/chat-page.js +0 -942
  88. package/dist/src/reskin/discover-page.js +0 -1788
  89. package/dist/src/reskin/document.js +0 -1587
  90. package/dist/src/reskin/help-page.js +0 -357
  91. package/dist/src/reskin/index-page.js +0 -62
  92. package/dist/src/reskin/inpaint-clipper-page.js +0 -890
  93. package/dist/src/reskin/inpaint-page.js +0 -2554
  94. package/dist/src/reskin/inpaint-video-page.js +0 -1339
  95. package/dist/src/reskin/job-runs-page.js +0 -477
  96. package/dist/src/reskin/library-page.js +0 -1634
  97. package/dist/src/reskin/login-page.js +0 -262
  98. package/dist/src/reskin/portfolio-page.js +0 -687
  99. package/dist/src/reskin/pricing-page.js +0 -390
  100. package/dist/src/reskin/settings-page.js +0 -732
  101. package/dist/src/reskin/theme.js +0 -711
  102. package/dist/src/runtime.js +0 -35
  103. package/dist/src/services/api-call-history.js +0 -249
  104. package/dist/src/services/auth.js +0 -152
  105. package/dist/src/services/billing-pricing.js +0 -39
  106. package/dist/src/services/billing.js +0 -241
  107. package/dist/src/services/cast.js +0 -127
  108. package/dist/src/services/chat-threads.js +0 -92
  109. package/dist/src/services/clip-records.js +0 -250
  110. package/dist/src/services/clip-search.js +0 -77
  111. package/dist/src/services/clip-vectors.js +0 -125
  112. package/dist/src/services/composition-sanitize.js +0 -124
  113. package/dist/src/services/composition-watch.js +0 -79
  114. package/dist/src/services/elevenlabs.js +0 -222
  115. package/dist/src/services/file-directory.js +0 -117
  116. package/dist/src/services/fork-access.js +0 -93
  117. package/dist/src/services/fork-manifest.js +0 -42
  118. package/dist/src/services/ghostcut.js +0 -179
  119. package/dist/src/services/hyperframes.js +0 -3654
  120. package/dist/src/services/job-capacity.js +0 -14
  121. package/dist/src/services/job-logs.js +0 -197
  122. package/dist/src/services/jobs.js +0 -136
  123. package/dist/src/services/local-dynamo.js +0 -0
  124. package/dist/src/services/media-processing.js +0 -766
  125. package/dist/src/services/primitive-media-lambda.js +0 -280
  126. package/dist/src/services/providers.js +0 -2748
  127. package/dist/src/services/rate-limits.js +0 -262
  128. package/dist/src/services/scene-annotations.js +0 -32
  129. package/dist/src/services/serverless-auth.js +0 -382
  130. package/dist/src/services/serverless-jobs.js +0 -1084
  131. package/dist/src/services/serverless-provider-keys.js +0 -409
  132. package/dist/src/services/serverless-records.js +0 -1515
  133. package/dist/src/services/serverless-template-configs.js +0 -75
  134. package/dist/src/services/storage.js +0 -461
  135. package/dist/src/services/swipe-customize.js +0 -437
  136. package/dist/src/services/template-certification.js +0 -413
  137. package/dist/src/services/template-loader.js +0 -99
  138. package/dist/src/services/template-runtime-bundles.js +0 -217
  139. package/dist/src/services/template-sources.js +0 -1017
  140. package/dist/src/services/upstream.js +0 -248
  141. package/dist/src/services/video-normalization.js +0 -2
  142. package/dist/src/services/webhooks.js +0 -62
  143. package/dist/src/template-editor-pages.js +0 -2576
  144. package/dist/src/template-editor-shell.js +0 -2893
  145. package/dist/src/template-sdk.js +0 -4
  146. package/dist/src/worker.js +0 -17
  147. package/public/assets/discover-client-app.js +0 -1
  148. package/public/assets/file-directory-app.js +0 -3
  149. package/public/assets/homepage-app.js +0 -54
  150. package/public/assets/homepage-client-app.js +0 -80
  151. package/public/assets/page-runtime-client-app.js +0 -94
  152. package/public/assets/placeholders/scene-placeholder.png +0 -0
  153. package/src/assets/SELLING_AWARENESS_STAGES.md +0 -579
  154. package/src/assets/SELLING_WITH_HOOKS.md +0 -377
  155. package/src/assets/SELLING_WITH_VSLS.md +0 -606
  156. package/src/assets/favicon.ico +0 -0
  157. package/src/assets/logo-vidfarm.png +0 -0
@@ -1,766 +0,0 @@
1
- import { spawn } from "node:child_process";
2
- import { createWriteStream } from "node:fs";
3
- import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
4
- import os from "node:os";
5
- import path from "node:path";
6
- import { Readable } from "node:stream";
7
- import { pipeline } from "node:stream/promises";
8
- import ffmpegPath from "ffmpeg-static";
9
- import ffprobe from "ffprobe-static";
10
- const DEFAULT_MAX_WIDTH = 1080;
11
- const DEFAULT_MAX_HEIGHT = 1920;
12
- const DEFAULT_CRF = 23;
13
- const DEFAULT_MAX_VIDEO_BITRATE_KBPS = 8_000;
14
- const DEFAULT_AUDIO_BITRATE_KBPS = 128;
15
- const DEFAULT_AUDIO_SAMPLE_RATE = 48_000;
16
- // Hard cap on probeable media duration. Every downstream consumer of
17
- // probeMediaAsset (auto-decompose, ingest, primitive video_probe / audio_probe)
18
- // is metered on downstream compute — a caller probing hour-long videos in a
19
- // loop is disproportionately expensive relative to REST rate limits. Enforced
20
- // inside the probe itself so no call site can accidentally bypass it.
21
- export const MAX_PROBE_DURATION_SECONDS = 120;
22
- export class MediaDurationExceededError extends Error {
23
- durationSeconds;
24
- maxDurationSeconds;
25
- constructor(durationSeconds, maxDurationSeconds) {
26
- super(`Media duration ${durationSeconds.toFixed(1)}s exceeds ${maxDurationSeconds}s limit`);
27
- this.name = "MediaDurationExceededError";
28
- this.durationSeconds = durationSeconds;
29
- this.maxDurationSeconds = maxDurationSeconds;
30
- }
31
- }
32
- const resolvedFfmpegPath = typeof ffmpegPath === "string" && ffmpegPath.trim() ? ffmpegPath : "ffmpeg";
33
- const ffprobePath = typeof ffprobe.path === "string" ? ffprobe.path : "";
34
- const resolvedFfprobePath = ffprobePath.trim() ? ffprobePath : "ffprobe";
35
- export function estimateNormalizationCostUsd(metadata) {
36
- const durationSeconds = Math.max(metadata.source.durationSeconds ?? 0, metadata.normalized.durationSeconds ?? 0);
37
- const largerFootprintMb = Math.max(metadata.source.bytes, metadata.normalized.bytes) / (1024 * 1024);
38
- const estimated = 0.003 + (durationSeconds * 0.0001) + (largerFootprintMb * 0.00005);
39
- return Number(estimated.toFixed(4));
40
- }
41
- export async function probeMediaAsset(input) {
42
- const maxDurationSeconds = input.maxDurationSeconds ?? MAX_PROBE_DURATION_SECONDS;
43
- const tempDir = await mkdtemp(path.join(os.tmpdir(), "vidfarm-media-probe-"));
44
- const sourcePath = path.join(tempDir, "source.bin");
45
- try {
46
- const sourceBytes = await downloadUrlToFile(input.sourceUrl, sourcePath);
47
- const metadata = await ffprobeMedia(sourcePath);
48
- const durationSeconds = metadata.durationSeconds;
49
- if (Number.isFinite(durationSeconds) &&
50
- durationSeconds !== null &&
51
- durationSeconds > maxDurationSeconds) {
52
- throw new MediaDurationExceededError(durationSeconds, maxDurationSeconds);
53
- }
54
- return {
55
- metadata: {
56
- ...metadata,
57
- bytes: sourceBytes,
58
- url: input.sourceUrl,
59
- contentType: inferContentTypeFromPath(sourcePath, metadata)
60
- }
61
- };
62
- }
63
- finally {
64
- await rm(tempDir, { recursive: true, force: true });
65
- }
66
- }
67
- export async function normalizeVideoAsset(input) {
68
- const tempDir = await mkdtemp(path.join(os.tmpdir(), "vidfarm-video-normalize-"));
69
- const sourcePath = path.join(tempDir, "source.bin");
70
- const outputPath = path.join(tempDir, "normalized.mp4");
71
- const preset = input.preset ?? "veryfast";
72
- try {
73
- const sourceBytes = await downloadUrlToFile(input.sourceUrl, sourcePath);
74
- const sourceProbe = await ffprobeMedia(sourcePath);
75
- await runFfmpeg([
76
- "-hide_banner",
77
- "-y",
78
- "-i", sourcePath,
79
- "-map", "0:v:0",
80
- "-map", "0:a:0?",
81
- "-vf", buildNormalizationVideoFilter({
82
- width: sourceProbe.width,
83
- height: sourceProbe.height,
84
- maxWidth: input.maxWidth ?? DEFAULT_MAX_WIDTH,
85
- maxHeight: input.maxHeight ?? DEFAULT_MAX_HEIGHT
86
- }),
87
- "-c:v", "libx264",
88
- "-preset", preset,
89
- "-crf", String(input.crf ?? DEFAULT_CRF),
90
- "-maxrate", `${input.maxVideoBitrateKbps ?? DEFAULT_MAX_VIDEO_BITRATE_KBPS}k`,
91
- "-bufsize", `${(input.maxVideoBitrateKbps ?? DEFAULT_MAX_VIDEO_BITRATE_KBPS) * 2}k`,
92
- "-pix_fmt", "yuv420p",
93
- "-movflags", "+faststart",
94
- "-g", "60",
95
- "-keyint_min", "60",
96
- "-sc_threshold", "0",
97
- "-c:a", "aac",
98
- "-b:a", `${input.audioBitrateKbps ?? DEFAULT_AUDIO_BITRATE_KBPS}k`,
99
- "-ar", String(DEFAULT_AUDIO_SAMPLE_RATE),
100
- outputPath
101
- ]);
102
- const outputBytes = await readFile(outputPath);
103
- const normalizedProbe = await ffprobeMedia(outputPath);
104
- return {
105
- bytes: outputBytes,
106
- contentType: "video/mp4",
107
- metadata: {
108
- source: {
109
- ...sourceProbe,
110
- bytes: sourceBytes,
111
- url: input.sourceUrl
112
- },
113
- normalized: {
114
- ...normalizedProbe,
115
- bytes: outputBytes.byteLength
116
- },
117
- compressionRatio: sourceBytes > 0 ? outputBytes.byteLength / sourceBytes : null
118
- }
119
- };
120
- }
121
- finally {
122
- await rm(tempDir, { recursive: true, force: true });
123
- }
124
- }
125
- export async function trimVideoAsset(input) {
126
- const tempDir = await mkdtemp(path.join(os.tmpdir(), "vidfarm-video-trim-"));
127
- const sourcePath = path.join(tempDir, "source.bin");
128
- const outputPath = path.join(tempDir, "trimmed.mp4");
129
- try {
130
- const sourceBytes = await downloadUrlToFile(input.sourceUrl, sourcePath);
131
- const sourceProbe = await ffprobeMedia(sourcePath);
132
- const clip = resolveTimeWindow(input);
133
- const startSeconds = clip.startMs / 1000;
134
- const durationSeconds = clip.durationMs / 1000;
135
- await runFfmpeg([
136
- "-hide_banner",
137
- "-y",
138
- "-ss", formatSeconds(startSeconds),
139
- "-i", sourcePath,
140
- "-t", formatSeconds(durationSeconds),
141
- "-map", "0:v:0",
142
- "-map", "0:a:0?",
143
- "-vf", buildNormalizationVideoFilter({
144
- width: sourceProbe.width,
145
- height: sourceProbe.height,
146
- maxWidth: input.maxWidth ?? sourceProbe.width ?? DEFAULT_MAX_WIDTH,
147
- maxHeight: input.maxHeight ?? sourceProbe.height ?? DEFAULT_MAX_HEIGHT
148
- }),
149
- "-c:v", "libx264",
150
- "-preset", input.preset ?? "veryfast",
151
- "-crf", String(input.crf ?? DEFAULT_CRF),
152
- "-pix_fmt", "yuv420p",
153
- "-movflags", "+faststart",
154
- "-c:a", "aac",
155
- "-b:a", `${input.audioBitrateKbps ?? DEFAULT_AUDIO_BITRATE_KBPS}k`,
156
- "-ar", String(DEFAULT_AUDIO_SAMPLE_RATE),
157
- outputPath
158
- ]);
159
- const outputBytes = await readFile(outputPath);
160
- const trimmedProbe = await ffprobeMedia(outputPath);
161
- return {
162
- bytes: outputBytes,
163
- contentType: "video/mp4",
164
- metadata: {
165
- source: { ...sourceProbe, bytes: sourceBytes, url: input.sourceUrl },
166
- trimmed: { ...trimmedProbe, bytes: outputBytes.byteLength },
167
- startMs: clip.startMs,
168
- durationMs: clip.durationMs,
169
- endMs: clip.startMs + clip.durationMs
170
- }
171
- };
172
- }
173
- finally {
174
- await rm(tempDir, { recursive: true, force: true });
175
- }
176
- }
177
- export async function extractAudioFromVideoAsset(input) {
178
- const tempDir = await mkdtemp(path.join(os.tmpdir(), "vidfarm-extract-audio-"));
179
- const sourcePath = path.join(tempDir, "source.bin");
180
- const format = input.format ?? "mp3";
181
- const outputPath = path.join(tempDir, `audio.${audioExtension(format)}`);
182
- try {
183
- const sourceBytes = await downloadUrlToFile(input.sourceUrl, sourcePath);
184
- const sourceProbe = await ffprobeMedia(sourcePath);
185
- await runFfmpeg([
186
- "-hide_banner",
187
- "-y",
188
- "-i", sourcePath,
189
- "-vn",
190
- ...audioEncodeArgs(format, input.bitrateKbps),
191
- outputPath
192
- ]);
193
- const outputBytes = await readFile(outputPath);
194
- const outputProbe = await ffprobeMedia(outputPath);
195
- return {
196
- bytes: outputBytes,
197
- contentType: audioContentType(format),
198
- metadata: {
199
- source: { ...sourceProbe, bytes: sourceBytes, url: input.sourceUrl },
200
- extracted: { ...outputProbe, bytes: outputBytes.byteLength },
201
- format
202
- }
203
- };
204
- }
205
- finally {
206
- await rm(tempDir, { recursive: true, force: true });
207
- }
208
- }
209
- export async function trimAudioAsset(input) {
210
- const tempDir = await mkdtemp(path.join(os.tmpdir(), "vidfarm-audio-trim-"));
211
- const sourcePath = path.join(tempDir, "source.bin");
212
- const format = input.format ?? "mp3";
213
- const outputPath = path.join(tempDir, `trimmed.${audioExtension(format)}`);
214
- try {
215
- const sourceBytes = await downloadUrlToFile(input.sourceUrl, sourcePath);
216
- const sourceProbe = await ffprobeMedia(sourcePath);
217
- const clip = resolveTimeWindow(input);
218
- await runFfmpeg([
219
- "-hide_banner",
220
- "-y",
221
- "-ss", formatSeconds(clip.startMs / 1000),
222
- "-i", sourcePath,
223
- "-t", formatSeconds(clip.durationMs / 1000),
224
- ...audioEncodeArgs(format, input.bitrateKbps),
225
- outputPath
226
- ]);
227
- const outputBytes = await readFile(outputPath);
228
- const outputProbe = await ffprobeMedia(outputPath);
229
- return {
230
- bytes: outputBytes,
231
- contentType: audioContentType(format),
232
- metadata: {
233
- source: { ...sourceProbe, bytes: sourceBytes, url: input.sourceUrl },
234
- trimmed: { ...outputProbe, bytes: outputBytes.byteLength },
235
- startMs: clip.startMs,
236
- durationMs: clip.durationMs,
237
- endMs: clip.startMs + clip.durationMs,
238
- format
239
- }
240
- };
241
- }
242
- finally {
243
- await rm(tempDir, { recursive: true, force: true });
244
- }
245
- }
246
- export async function normalizeAudioAsset(input) {
247
- const tempDir = await mkdtemp(path.join(os.tmpdir(), "vidfarm-audio-normalize-"));
248
- const sourcePath = path.join(tempDir, "source.bin");
249
- const format = input.format ?? "mp3";
250
- const outputPath = path.join(tempDir, `normalized.${audioExtension(format)}`);
251
- try {
252
- const sourceBytes = await downloadUrlToFile(input.sourceUrl, sourcePath);
253
- const sourceProbe = await ffprobeMedia(sourcePath);
254
- await runFfmpeg([
255
- "-hide_banner",
256
- "-y",
257
- "-i", sourcePath,
258
- "-af", "loudnorm=I=-16:LRA=11:TP=-1.5",
259
- ...audioEncodeArgs(format, input.bitrateKbps, input.sampleRateHz ?? DEFAULT_AUDIO_SAMPLE_RATE),
260
- outputPath
261
- ]);
262
- const outputBytes = await readFile(outputPath);
263
- const outputProbe = await ffprobeMedia(outputPath);
264
- return {
265
- bytes: outputBytes,
266
- contentType: audioContentType(format),
267
- metadata: {
268
- source: { ...sourceProbe, bytes: sourceBytes, url: input.sourceUrl },
269
- normalized: { ...outputProbe, bytes: outputBytes.byteLength },
270
- format
271
- }
272
- };
273
- }
274
- finally {
275
- await rm(tempDir, { recursive: true, force: true });
276
- }
277
- }
278
- export async function muteVideoAsset(input) {
279
- const tempDir = await mkdtemp(path.join(os.tmpdir(), "vidfarm-video-mute-"));
280
- const sourcePath = path.join(tempDir, "source.bin");
281
- const outputPath = path.join(tempDir, "muted.mp4");
282
- try {
283
- const sourceBytes = await downloadUrlToFile(input.sourceUrl, sourcePath);
284
- const sourceProbe = await ffprobeMedia(sourcePath);
285
- await runFfmpeg([
286
- "-hide_banner",
287
- "-y",
288
- "-i", sourcePath,
289
- "-map", "0:v:0",
290
- "-an",
291
- "-c:v", "copy",
292
- "-movflags", "+faststart",
293
- outputPath
294
- ]);
295
- const outputBytes = await readFile(outputPath);
296
- const outputProbe = await ffprobeMedia(outputPath);
297
- return {
298
- bytes: outputBytes,
299
- contentType: "video/mp4",
300
- metadata: {
301
- source: { ...sourceProbe, bytes: sourceBytes, url: input.sourceUrl },
302
- muted: { ...outputProbe, bytes: outputBytes.byteLength }
303
- }
304
- };
305
- }
306
- finally {
307
- await rm(tempDir, { recursive: true, force: true });
308
- }
309
- }
310
- export async function replaceVideoAudioAsset(input) {
311
- const tempDir = await mkdtemp(path.join(os.tmpdir(), "vidfarm-video-replace-audio-"));
312
- const videoPath = path.join(tempDir, "video.bin");
313
- const audioPath = path.join(tempDir, "audio.bin");
314
- const outputPath = path.join(tempDir, "output.mp4");
315
- try {
316
- const videoBytes = await downloadUrlToFile(input.sourceVideoUrl, videoPath);
317
- const audioBytes = await downloadUrlToFile(input.audioUrl, audioPath);
318
- const videoProbe = await ffprobeMedia(videoPath);
319
- const audioProbe = await ffprobeMedia(audioPath);
320
- await runFfmpeg([
321
- "-hide_banner",
322
- "-y",
323
- "-i", videoPath,
324
- "-i", audioPath,
325
- "-map", "0:v:0",
326
- "-map", "1:a:0",
327
- "-c:v", "copy",
328
- "-c:a", "aac",
329
- "-b:a", `${input.audioBitrateKbps ?? DEFAULT_AUDIO_BITRATE_KBPS}k`,
330
- "-shortest",
331
- "-movflags", "+faststart",
332
- outputPath
333
- ]);
334
- const outputBytes = await readFile(outputPath);
335
- const outputProbe = await ffprobeMedia(outputPath);
336
- return {
337
- bytes: outputBytes,
338
- contentType: "video/mp4",
339
- metadata: {
340
- sourceVideo: { ...videoProbe, bytes: videoBytes, url: input.sourceVideoUrl },
341
- sourceAudio: { ...audioProbe, bytes: audioBytes, url: input.audioUrl },
342
- replaced: { ...outputProbe, bytes: outputBytes.byteLength }
343
- }
344
- };
345
- }
346
- finally {
347
- await rm(tempDir, { recursive: true, force: true });
348
- }
349
- }
350
- export async function extractVideoFrameAsset(input) {
351
- const tempDir = await mkdtemp(path.join(os.tmpdir(), "vidfarm-video-frame-"));
352
- const sourcePath = path.join(tempDir, "source.bin");
353
- const format = input.format ?? "png";
354
- const outputPath = path.join(tempDir, `frame.${format === "jpeg" ? "jpg" : format}`);
355
- try {
356
- const sourceBytes = await downloadUrlToFile(input.sourceUrl, sourcePath);
357
- const sourceProbe = await ffprobeMedia(sourcePath);
358
- await runFfmpeg([
359
- "-hide_banner",
360
- "-y",
361
- ...(typeof input.atMs === "number" && input.atMs > 0 ? ["-ss", formatSeconds(input.atMs / 1000)] : []),
362
- "-i", sourcePath,
363
- "-frames:v", "1",
364
- outputPath
365
- ]);
366
- const outputBytes = await readFile(outputPath);
367
- return {
368
- bytes: outputBytes,
369
- contentType: format === "jpeg" ? "image/jpeg" : "image/png",
370
- metadata: {
371
- source: { ...sourceProbe, bytes: sourceBytes, url: input.sourceUrl },
372
- atMs: Math.max(0, Math.round(input.atMs ?? 0)),
373
- format
374
- }
375
- };
376
- }
377
- finally {
378
- await rm(tempDir, { recursive: true, force: true });
379
- }
380
- }
381
- export async function concatAudioAssets(input) {
382
- if (!input.sourceUrls.length) {
383
- throw new Error("At least one audio source URL is required.");
384
- }
385
- const tempDir = await mkdtemp(path.join(os.tmpdir(), "vidfarm-audio-concat-"));
386
- const format = input.format ?? "mp3";
387
- const outputPath = path.join(tempDir, `concat.${audioExtension(format)}`);
388
- try {
389
- const inputs = [];
390
- for (let index = 0; index < input.sourceUrls.length; index += 1) {
391
- const filePath = path.join(tempDir, `audio-${String(index + 1).padStart(3, "0")}.bin`);
392
- const bytes = await downloadUrlToFile(input.sourceUrls[index], filePath);
393
- const metadata = await ffprobeMedia(filePath);
394
- inputs.push({ url: input.sourceUrls[index], filePath, bytes, metadata });
395
- }
396
- const args = ["-hide_banner", "-y"];
397
- for (const item of inputs) {
398
- args.push("-i", item.filePath);
399
- }
400
- const filterInputs = inputs.map((_, index) => `[${index}:a]`).join("");
401
- args.push("-filter_complex", `${filterInputs}concat=n=${inputs.length}:v=0:a=1[outa]`, "-map", "[outa]", ...audioEncodeArgs(format, input.bitrateKbps), outputPath);
402
- await runFfmpeg(args);
403
- const outputBytes = await readFile(outputPath);
404
- const outputProbe = await ffprobeMedia(outputPath);
405
- return {
406
- bytes: outputBytes,
407
- contentType: audioContentType(format),
408
- metadata: {
409
- sources: inputs.map((item) => ({ ...item.metadata, bytes: item.bytes, url: item.url })),
410
- concatenated: { ...outputProbe, bytes: outputBytes.byteLength },
411
- format,
412
- sourceCount: inputs.length
413
- }
414
- };
415
- }
416
- finally {
417
- await rm(tempDir, { recursive: true, force: true });
418
- }
419
- }
420
- export async function concatVideoAssets(input) {
421
- if (!input.sourceUrls.length) {
422
- throw new Error("At least one video source URL is required.");
423
- }
424
- const tempDir = await mkdtemp(path.join(os.tmpdir(), "vidfarm-video-concat-"));
425
- const outputPath = path.join(tempDir, "concat.mp4");
426
- try {
427
- const inputs = [];
428
- for (let index = 0; index < input.sourceUrls.length; index += 1) {
429
- const filePath = path.join(tempDir, `video-${String(index + 1).padStart(3, "0")}.bin`);
430
- const bytes = await downloadUrlToFile(input.sourceUrls[index], filePath);
431
- const metadata = await ffprobeMedia(filePath);
432
- inputs.push({ url: input.sourceUrls[index], filePath, bytes, metadata });
433
- }
434
- const width = Math.max(16, Math.round(input.width ?? inputs[0]?.metadata.width ?? DEFAULT_MAX_WIDTH));
435
- const height = Math.max(16, Math.round(input.height ?? inputs[0]?.metadata.height ?? DEFAULT_MAX_HEIGHT));
436
- const fit = input.fit ?? "contain";
437
- const backgroundColor = normalizeFfmpegColor(input.backgroundColor || "black");
438
- const args = ["-hide_banner", "-y"];
439
- for (const item of inputs) {
440
- args.push("-i", item.filePath);
441
- }
442
- const filters = inputs.map((_, index) => (`[${index}:v]${videoCanvasFilter({ width, height, fit, backgroundColor })}[v${index}]`));
443
- const concatInputs = inputs.map((_, index) => `[v${index}]`).join("");
444
- args.push("-filter_complex", `${filters.join(";")};${concatInputs}concat=n=${inputs.length}:v=1:a=0[outv]`, "-map", "[outv]", "-an", "-r", "30", "-c:v", "libx264", "-pix_fmt", "yuv420p", "-movflags", "+faststart", outputPath);
445
- await runFfmpeg(args);
446
- const outputBytes = await readFile(outputPath);
447
- const outputProbe = await ffprobeMedia(outputPath);
448
- return {
449
- bytes: outputBytes,
450
- contentType: "video/mp4",
451
- metadata: {
452
- sources: inputs.map((item) => ({ ...item.metadata, bytes: item.bytes, url: item.url })),
453
- concatenated: { ...outputProbe, bytes: outputBytes.byteLength },
454
- width,
455
- height,
456
- fit,
457
- sourceCount: inputs.length,
458
- includesAudio: false
459
- }
460
- };
461
- }
462
- finally {
463
- await rm(tempDir, { recursive: true, force: true });
464
- }
465
- }
466
- export async function renderSlidesVideoAsset(input) {
467
- if (!input.slides.length) {
468
- throw new Error("At least one slide is required.");
469
- }
470
- const tempDir = await mkdtemp(path.join(os.tmpdir(), "vidfarm-primitive-slides-"));
471
- const outputPath = path.join(tempDir, "output.mp4");
472
- try {
473
- const downloadedSlides = [];
474
- for (let index = 0; index < input.slides.length; index += 1) {
475
- const slide = input.slides[index];
476
- const filePath = path.join(tempDir, `slide-${String(index + 1).padStart(3, "0")}${extensionForMediaUrl(slide.mediaUrl, slide.mediaType)}`);
477
- await downloadMediaToFile(slide.mediaUrl, filePath);
478
- downloadedSlides.push({ ...slide, filePath });
479
- }
480
- await runFfmpeg(buildSlideRenderArgs({
481
- slides: downloadedSlides,
482
- outputPath,
483
- width: input.width,
484
- height: input.height,
485
- backgroundColor: input.backgroundColor,
486
- defaultFit: input.defaultFit
487
- }));
488
- const outputBytes = await readFile(outputPath);
489
- const outputProbe = await ffprobeMedia(outputPath);
490
- return {
491
- bytes: outputBytes,
492
- contentType: "video/mp4",
493
- metadata: {
494
- rendered: { ...outputProbe, bytes: outputBytes.byteLength },
495
- width: input.width,
496
- height: input.height,
497
- slideCount: input.slides.length,
498
- durationMs: input.slides.reduce((sum, slide) => sum + slide.durationMs, 0)
499
- }
500
- };
501
- }
502
- finally {
503
- await rm(tempDir, { recursive: true, force: true });
504
- }
505
- }
506
- function buildNormalizationVideoFilter(input) {
507
- const width = input.width ?? input.maxWidth;
508
- const height = input.height ?? input.maxHeight;
509
- if (width <= input.maxWidth && height <= input.maxHeight) {
510
- return "scale=trunc(iw/2)*2:trunc(ih/2)*2,setsar=1";
511
- }
512
- return `scale=w=${input.maxWidth}:h=${input.maxHeight}:force_original_aspect_ratio=decrease:force_divisible_by=2,setsar=1`;
513
- }
514
- function resolveTimeWindow(input) {
515
- const startMs = Math.max(0, Math.round(input.startMs ?? 0));
516
- const durationMs = input.durationMs == null
517
- ? Math.max(1, Math.round((input.endMs ?? 0) - startMs))
518
- : Math.max(1, Math.round(input.durationMs));
519
- if (input.durationMs == null && (input.endMs == null || input.endMs <= startMs)) {
520
- throw new Error("Provide either durationMs or an endMs greater than startMs.");
521
- }
522
- return {
523
- startMs,
524
- durationMs
525
- };
526
- }
527
- function audioEncodeArgs(format, bitrateKbps, sampleRateHz = DEFAULT_AUDIO_SAMPLE_RATE) {
528
- if (format === "wav") {
529
- return ["-c:a", "pcm_s16le", "-ar", String(sampleRateHz)];
530
- }
531
- if (format === "m4a") {
532
- return ["-c:a", "aac", "-b:a", `${bitrateKbps ?? DEFAULT_AUDIO_BITRATE_KBPS}k`, "-ar", String(sampleRateHz)];
533
- }
534
- return ["-c:a", "libmp3lame", "-b:a", `${bitrateKbps ?? DEFAULT_AUDIO_BITRATE_KBPS}k`, "-ar", String(sampleRateHz)];
535
- }
536
- function audioContentType(format) {
537
- if (format === "wav")
538
- return "audio/wav";
539
- if (format === "m4a")
540
- return "audio/mp4";
541
- return "audio/mpeg";
542
- }
543
- function audioExtension(format) {
544
- if (format === "wav")
545
- return "wav";
546
- if (format === "m4a")
547
- return "m4a";
548
- return "mp3";
549
- }
550
- function videoCanvasFilter(input) {
551
- if (input.fit === "fill") {
552
- return `scale=${input.width}:${input.height},setsar=1,fps=30,setpts=PTS-STARTPTS`;
553
- }
554
- if (input.fit === "contain") {
555
- return `scale=${input.width}:${input.height}:force_original_aspect_ratio=decrease,pad=${input.width}:${input.height}:(ow-iw)/2:(oh-ih)/2:color=${input.backgroundColor},setsar=1,fps=30,setpts=PTS-STARTPTS`;
556
- }
557
- return `scale=${input.width}:${input.height}:force_original_aspect_ratio=increase,crop=${input.width}:${input.height},setsar=1,fps=30,setpts=PTS-STARTPTS`;
558
- }
559
- function buildSlideRenderArgs(input) {
560
- const args = ["-hide_banner", "-y"];
561
- for (const slide of input.slides) {
562
- const durationSeconds = slide.durationMs / 1000;
563
- if (slide.mediaType === "video") {
564
- args.push("-stream_loop", "-1", "-t", durationSeconds.toFixed(3), "-i", slide.filePath);
565
- }
566
- else {
567
- args.push("-loop", "1", "-t", durationSeconds.toFixed(3), "-i", slide.filePath);
568
- }
569
- }
570
- const filters = input.slides.map((slide, index) => {
571
- const durationSeconds = slide.durationMs / 1000;
572
- const fit = slide.fit ?? input.defaultFit;
573
- const backgroundColor = normalizeFfmpegColor(slide.backgroundColor || input.backgroundColor);
574
- return `[${index}:v]${primitiveSlideVideoFilter({
575
- width: input.width,
576
- height: input.height,
577
- fit,
578
- backgroundColor,
579
- durationSeconds
580
- })}[v${index}]`;
581
- });
582
- const concatInputs = input.slides.map((_, index) => `[v${index}]`).join("");
583
- const filterComplex = `${filters.join(";")};${concatInputs}concat=n=${input.slides.length}:v=1:a=0[outv]`;
584
- args.push("-filter_complex", filterComplex, "-map", "[outv]", "-an", "-r", "30", "-c:v", "libx264", "-pix_fmt", "yuv420p", "-movflags", "+faststart", input.outputPath);
585
- return args;
586
- }
587
- function primitiveSlideVideoFilter(input) {
588
- const base = input.fit === "fill"
589
- ? `scale=${input.width}:${input.height}`
590
- : input.fit === "contain"
591
- ? `scale=${input.width}:${input.height}:force_original_aspect_ratio=decrease,pad=${input.width}:${input.height}:(ow-iw)/2:(oh-ih)/2:color=${input.backgroundColor}`
592
- : `scale=${input.width}:${input.height}:force_original_aspect_ratio=increase,crop=${input.width}:${input.height}`;
593
- return `${base},setsar=1,fps=30,trim=duration=${input.durationSeconds.toFixed(3)},setpts=PTS-STARTPTS`;
594
- }
595
- async function downloadMediaToFile(url, filePath) {
596
- const response = await fetch(url, { signal: AbortSignal.timeout(60_000) });
597
- if (!response.ok) {
598
- throw new Error(`Media returned ${response.status} ${response.statusText || "HTTP error"} for ${formatUrlForError(url)}`);
599
- }
600
- const bytes = new Uint8Array(await response.arrayBuffer());
601
- if (!bytes.byteLength) {
602
- throw new Error(`Media was empty for ${formatUrlForError(url)}`);
603
- }
604
- await writeFile(filePath, bytes);
605
- }
606
- export async function ffprobeMedia(filePath) {
607
- const stdout = await runCommand(resolvedFfprobePath, [
608
- "-v", "error",
609
- "-print_format", "json",
610
- "-show_streams",
611
- "-show_format",
612
- filePath
613
- ]);
614
- const parsed = JSON.parse(stdout);
615
- const streams = Array.isArray(parsed.streams) ? parsed.streams : [];
616
- const video = streams.find((stream) => stream.codec_type === "video") ?? null;
617
- const audio = streams.find((stream) => stream.codec_type === "audio") ?? null;
618
- const format = parsed.format ?? {};
619
- return {
620
- durationSeconds: parseNullableNumber(format.duration),
621
- width: parseNullableInteger(video?.width),
622
- height: parseNullableInteger(video?.height),
623
- videoCodec: typeof video?.codec_name === "string" ? video.codec_name : null,
624
- audioCodec: typeof audio?.codec_name === "string" ? audio.codec_name : null,
625
- videoBitrateKbps: toKbps(parseNullableNumber(video?.bit_rate)),
626
- audioBitrateKbps: toKbps(parseNullableNumber(audio?.bit_rate)),
627
- formatBitrateKbps: toKbps(parseNullableNumber(format.bit_rate)),
628
- hasAudio: Boolean(audio),
629
- hasVideo: Boolean(video),
630
- sampleRateHz: parseNullableInteger(audio?.sample_rate),
631
- channels: parseNullableInteger(audio?.channels),
632
- frameRate: parseFrameRate(video?.avg_frame_rate ?? video?.r_frame_rate),
633
- formatName: typeof format.format_name === "string" ? format.format_name : null
634
- };
635
- }
636
- async function downloadUrlToFile(url, filePath) {
637
- const response = await fetch(url, { signal: AbortSignal.timeout(10 * 60 * 1000) });
638
- if (!response.ok) {
639
- throw new Error(`Media download returned ${response.status} ${response.statusText || "HTTP error"} for ${formatUrlForError(url)}`);
640
- }
641
- if (!response.body) {
642
- throw new Error(`Media download returned no body for ${formatUrlForError(url)}`);
643
- }
644
- const source = Readable.fromWeb(response.body);
645
- const sink = createWriteStream(filePath);
646
- await pipeline(source, sink);
647
- const bytes = Number.parseInt(response.headers.get("content-length") || "", 10);
648
- if (Number.isFinite(bytes) && bytes > 0) {
649
- return bytes;
650
- }
651
- const written = await readFile(filePath);
652
- return written.byteLength;
653
- }
654
- export async function runFfmpeg(args) {
655
- await runCommand(resolvedFfmpegPath, args, true);
656
- }
657
- async function runCommand(command, args, allowStderr = false) {
658
- const result = await new Promise((resolve, reject) => {
659
- const child = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });
660
- let stdout = "";
661
- let stderr = "";
662
- child.stdout.setEncoding("utf8");
663
- child.stderr.setEncoding("utf8");
664
- child.stdout.on("data", (chunk) => {
665
- stdout += chunk;
666
- if (stdout.length > 200_000)
667
- stdout = stdout.slice(-200_000);
668
- });
669
- child.stderr.on("data", (chunk) => {
670
- stderr += chunk;
671
- if (stderr.length > 100_000)
672
- stderr = stderr.slice(-100_000);
673
- });
674
- child.on("error", reject);
675
- child.on("close", (code) => resolve({ code, stdout, stderr }));
676
- });
677
- if (result.code !== 0) {
678
- throw new Error(`${path.basename(command)} failed with exit code ${result.code ?? "unknown"}: ${result.stderr.slice(-4000)}`);
679
- }
680
- if (!allowStderr && result.stderr.trim()) {
681
- return result.stdout;
682
- }
683
- return result.stdout;
684
- }
685
- function parseNullableNumber(value) {
686
- if (typeof value === "number" && Number.isFinite(value))
687
- return value;
688
- if (typeof value === "string") {
689
- const parsed = Number.parseFloat(value);
690
- return Number.isFinite(parsed) ? parsed : null;
691
- }
692
- return null;
693
- }
694
- function parseNullableInteger(value) {
695
- if (typeof value === "number" && Number.isFinite(value))
696
- return Math.trunc(value);
697
- if (typeof value === "string") {
698
- const parsed = Number.parseInt(value, 10);
699
- return Number.isFinite(parsed) ? parsed : null;
700
- }
701
- return null;
702
- }
703
- function parseFrameRate(value) {
704
- if (typeof value === "number" && Number.isFinite(value) && value > 0) {
705
- return value;
706
- }
707
- if (typeof value !== "string" || !value.includes("/")) {
708
- return null;
709
- }
710
- const [numeratorText, denominatorText] = value.split("/");
711
- const numerator = Number.parseFloat(numeratorText || "");
712
- const denominator = Number.parseFloat(denominatorText || "");
713
- if (!Number.isFinite(numerator) || !Number.isFinite(denominator) || denominator <= 0) {
714
- return null;
715
- }
716
- return Number((numerator / denominator).toFixed(4));
717
- }
718
- function toKbps(value) {
719
- return value == null ? null : Math.round(value / 1000);
720
- }
721
- function formatSeconds(value) {
722
- return value.toFixed(3);
723
- }
724
- function extensionForMediaUrl(mediaUrl, mediaType) {
725
- try {
726
- const extension = path.extname(new URL(mediaUrl).pathname).toLowerCase();
727
- if (/^\.[a-z0-9]{2,5}$/i.test(extension)) {
728
- return extension;
729
- }
730
- }
731
- catch {
732
- // Fall through to defaults.
733
- }
734
- return mediaType === "video" ? ".mp4" : ".img";
735
- }
736
- function normalizeFfmpegColor(value) {
737
- const trimmed = value.trim();
738
- const hex = /^#?([0-9a-f]{6})$/i.exec(trimmed);
739
- if (hex) {
740
- return `0x${hex[1]}`;
741
- }
742
- return /^[a-z]+$/i.test(trimmed) ? trimmed : "black";
743
- }
744
- function inferContentTypeFromPath(filePath, metadata) {
745
- if (metadata.hasVideo) {
746
- if (filePath.endsWith(".webm"))
747
- return "video/webm";
748
- if (filePath.endsWith(".mov"))
749
- return "video/quicktime";
750
- return "video/mp4";
751
- }
752
- if (metadata.hasAudio) {
753
- if (filePath.endsWith(".wav"))
754
- return "audio/wav";
755
- if (filePath.endsWith(".m4a"))
756
- return "audio/mp4";
757
- if (filePath.endsWith(".ogg"))
758
- return "audio/ogg";
759
- return "audio/mpeg";
760
- }
761
- return null;
762
- }
763
- function formatUrlForError(url) {
764
- return url.length > 300 ? `${url.slice(0, 300)}...` : url;
765
- }
766
- //# sourceMappingURL=media-processing.js.map