digital-workers 2.1.3 → 2.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.
Files changed (183) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +2 -0
  3. package/dist/actions.d.ts.map +1 -1
  4. package/dist/actions.js +33 -21
  5. package/dist/actions.js.map +1 -1
  6. package/dist/agent-comms.d.ts.map +1 -1
  7. package/dist/agent-comms.js +36 -25
  8. package/dist/agent-comms.js.map +1 -1
  9. package/dist/approve.d.ts +40 -8
  10. package/dist/approve.d.ts.map +1 -1
  11. package/dist/approve.js +86 -20
  12. package/dist/approve.js.map +1 -1
  13. package/dist/ask.d.ts +38 -7
  14. package/dist/ask.d.ts.map +1 -1
  15. package/dist/ask.js +85 -25
  16. package/dist/ask.js.map +1 -1
  17. package/dist/browse.d.ts +223 -0
  18. package/dist/browse.d.ts.map +1 -0
  19. package/dist/browse.js +392 -0
  20. package/dist/browse.js.map +1 -0
  21. package/dist/capability-tiers.js +3 -3
  22. package/dist/capability-tiers.js.map +1 -1
  23. package/dist/cascade-context.d.ts +28 -28
  24. package/dist/client.d.ts +162 -0
  25. package/dist/client.d.ts.map +1 -0
  26. package/dist/client.js +64 -0
  27. package/dist/client.js.map +1 -0
  28. package/dist/decide.d.ts +42 -6
  29. package/dist/decide.d.ts.map +1 -1
  30. package/dist/decide.js +54 -11
  31. package/dist/decide.js.map +1 -1
  32. package/dist/do.d.ts +36 -7
  33. package/dist/do.d.ts.map +1 -1
  34. package/dist/do.js +82 -39
  35. package/dist/do.js.map +1 -1
  36. package/dist/error-escalation.d.ts.map +1 -1
  37. package/dist/error-escalation.js +38 -38
  38. package/dist/error-escalation.js.map +1 -1
  39. package/dist/generate.d.ts +48 -7
  40. package/dist/generate.d.ts.map +1 -1
  41. package/dist/generate.js +49 -8
  42. package/dist/generate.js.map +1 -1
  43. package/dist/goals.d.ts +10 -9
  44. package/dist/goals.d.ts.map +1 -1
  45. package/dist/goals.js +30 -24
  46. package/dist/goals.js.map +1 -1
  47. package/dist/image.d.ts +189 -0
  48. package/dist/image.d.ts.map +1 -0
  49. package/dist/image.js +528 -0
  50. package/dist/image.js.map +1 -0
  51. package/dist/index.d.ts +49 -2
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +58 -2
  54. package/dist/index.js.map +1 -1
  55. package/dist/is.d.ts +45 -10
  56. package/dist/is.d.ts.map +1 -1
  57. package/dist/is.js +56 -21
  58. package/dist/is.js.map +1 -1
  59. package/dist/kpis.d.ts +24 -15
  60. package/dist/kpis.d.ts.map +1 -1
  61. package/dist/kpis.js +16 -14
  62. package/dist/kpis.js.map +1 -1
  63. package/dist/load-balancing.d.ts.map +1 -1
  64. package/dist/load-balancing.js +124 -38
  65. package/dist/load-balancing.js.map +1 -1
  66. package/dist/logger.d.ts +76 -0
  67. package/dist/logger.d.ts.map +1 -0
  68. package/dist/logger.js +39 -0
  69. package/dist/logger.js.map +1 -0
  70. package/dist/notify.d.ts +38 -9
  71. package/dist/notify.d.ts.map +1 -1
  72. package/dist/notify.js +72 -17
  73. package/dist/notify.js.map +1 -1
  74. package/dist/role.d.ts +5 -4
  75. package/dist/role.d.ts.map +1 -1
  76. package/dist/role.js +13 -10
  77. package/dist/role.js.map +1 -1
  78. package/dist/runtime.d.ts +310 -0
  79. package/dist/runtime.d.ts.map +1 -0
  80. package/dist/runtime.js +510 -0
  81. package/dist/runtime.js.map +1 -0
  82. package/dist/team.d.ts +11 -6
  83. package/dist/team.d.ts.map +1 -1
  84. package/dist/team.js +22 -15
  85. package/dist/team.js.map +1 -1
  86. package/dist/transports/email.d.ts +318 -0
  87. package/dist/transports/email.d.ts.map +1 -0
  88. package/dist/transports/email.js +779 -0
  89. package/dist/transports/email.js.map +1 -0
  90. package/dist/transports/slack.d.ts +515 -0
  91. package/dist/transports/slack.d.ts.map +1 -0
  92. package/dist/transports/slack.js +844 -0
  93. package/dist/transports/slack.js.map +1 -0
  94. package/dist/transports.d.ts.map +1 -1
  95. package/dist/transports.js +44 -25
  96. package/dist/transports.js.map +1 -1
  97. package/dist/types.d.ts +141 -19
  98. package/dist/types.d.ts.map +1 -1
  99. package/dist/types.js +5 -0
  100. package/dist/types.js.map +1 -1
  101. package/dist/utils/id.d.ts +19 -0
  102. package/dist/utils/id.d.ts.map +1 -0
  103. package/dist/utils/id.js +21 -0
  104. package/dist/utils/id.js.map +1 -0
  105. package/dist/video.d.ts +203 -0
  106. package/dist/video.d.ts.map +1 -0
  107. package/dist/video.js +528 -0
  108. package/dist/video.js.map +1 -0
  109. package/dist/worker.d.ts +343 -0
  110. package/dist/worker.d.ts.map +1 -0
  111. package/dist/worker.js +698 -0
  112. package/dist/worker.js.map +1 -0
  113. package/package.json +32 -14
  114. package/src/actions.ts +39 -30
  115. package/src/agent-comms.ts +54 -92
  116. package/src/approve.ts +91 -20
  117. package/src/ask.ts +99 -25
  118. package/src/browse.ts +627 -0
  119. package/src/capability-tiers.ts +5 -5
  120. package/src/client.ts +221 -0
  121. package/src/decide.ts +81 -35
  122. package/src/do.ts +98 -52
  123. package/src/error-escalation.ts +55 -67
  124. package/src/generate.ts +52 -18
  125. package/src/goals.ts +36 -27
  126. package/src/image.ts +816 -0
  127. package/src/index.ts +187 -2
  128. package/src/is.ts +59 -25
  129. package/src/kpis.ts +41 -36
  130. package/src/load-balancing.ts +132 -46
  131. package/src/logger.ts +93 -0
  132. package/src/notify.ts +78 -17
  133. package/src/role.ts +30 -20
  134. package/src/runtime.ts +796 -0
  135. package/src/team.ts +24 -19
  136. package/src/transports/email.ts +1160 -0
  137. package/src/transports/slack.ts +1320 -0
  138. package/src/transports.ts +58 -43
  139. package/src/types.ts +174 -46
  140. package/src/utils/id.ts +21 -0
  141. package/src/video.ts +906 -0
  142. package/src/worker.ts +1007 -0
  143. package/test/approve.test.ts +305 -0
  144. package/test/ask.test.ts +274 -0
  145. package/test/browse.test.ts +361 -0
  146. package/test/decide.test.ts +252 -0
  147. package/test/do.test.ts +144 -0
  148. package/test/error-logging.test.ts +357 -0
  149. package/test/generate.test.ts +319 -0
  150. package/test/image.test.ts +398 -0
  151. package/test/is.test.ts +287 -0
  152. package/test/load-balancing-safety.test.ts +404 -0
  153. package/test/notify.test.ts +434 -0
  154. package/test/primitives.test.ts +320 -0
  155. package/test/runtime-integration.test.ts +892 -0
  156. package/test/transports/crypto.test.ts +230 -0
  157. package/test/transports/email.test.ts +866 -0
  158. package/test/transports/id-generation.test.ts +91 -0
  159. package/test/transports/slack.test.ts +760 -0
  160. package/test/type-safety.test.ts +834 -0
  161. package/test/types.test.ts +60 -2
  162. package/test/video.test.ts +530 -0
  163. package/test/worker.test.ts +1433 -0
  164. package/tsconfig.json +4 -1
  165. package/vitest.config.ts +42 -0
  166. package/wrangler.jsonc +36 -0
  167. package/.turbo/turbo-build.log +0 -4
  168. package/LICENSE +0 -21
  169. package/src/actions.js +0 -436
  170. package/src/approve.js +0 -234
  171. package/src/ask.js +0 -226
  172. package/src/decide.js +0 -244
  173. package/src/do.js +0 -227
  174. package/src/generate.js +0 -298
  175. package/src/goals.js +0 -205
  176. package/src/index.js +0 -68
  177. package/src/is.js +0 -317
  178. package/src/kpis.js +0 -270
  179. package/src/notify.js +0 -219
  180. package/src/role.js +0 -110
  181. package/src/team.js +0 -130
  182. package/src/transports.js +0 -357
  183. package/src/types.js +0 -71
package/src/video.ts ADDED
@@ -0,0 +1,906 @@
1
+ /**
2
+ * Video generation functionality for digital workers
3
+ *
4
+ * This module provides video generation within a worker context,
5
+ * supporting various AI video generation models (Runway, Pika, etc.)
6
+ * with rich metadata about the generation process.
7
+ *
8
+ * @module
9
+ */
10
+
11
+ // ============================================================================
12
+ // Video Types
13
+ // ============================================================================
14
+
15
+ /**
16
+ * Video resolution options
17
+ */
18
+ export type VideoResolution = '480p' | '720p' | '1080p' | '4k'
19
+
20
+ /**
21
+ * Video aspect ratio options
22
+ */
23
+ export type VideoAspectRatio = '16:9' | '9:16' | '1:1' | '4:3' | '21:9'
24
+
25
+ /**
26
+ * Supported video generation models
27
+ */
28
+ export type VideoModel =
29
+ | 'runway-gen3'
30
+ | 'runway-gen2'
31
+ | 'pika-1.0'
32
+ | 'pika-1.5'
33
+ | 'stable-video'
34
+ | 'minimax'
35
+ | 'kling'
36
+ | 'luma'
37
+ | string
38
+
39
+ /**
40
+ * Video style presets
41
+ */
42
+ export type VideoStyle =
43
+ | 'cinematic'
44
+ | 'anime'
45
+ | 'realistic'
46
+ | 'cartoon'
47
+ | 'documentary'
48
+ | 'vintage'
49
+ | 'noir'
50
+ | 'fantasy'
51
+ | 'sci-fi'
52
+ | string
53
+
54
+ /**
55
+ * Options for video generation
56
+ */
57
+ export interface VideoOptions {
58
+ /** Text prompt describing the video to generate */
59
+ prompt: string
60
+ /** Video duration in seconds (default: 4) */
61
+ duration?: number
62
+ /** Frames per second (default: 24) */
63
+ fps?: number
64
+ /** Video resolution (default: '1080p') */
65
+ resolution?: VideoResolution
66
+ /** Aspect ratio (default: '16:9') */
67
+ aspectRatio?: VideoAspectRatio
68
+ /** Visual style preset */
69
+ style?: VideoStyle
70
+ /** AI model to use */
71
+ model?: VideoModel
72
+ /** Negative prompt - what to avoid */
73
+ negativePrompt?: string
74
+ /** Guidance scale for generation (1-20) */
75
+ guidance?: number
76
+ /** Random seed for reproducibility */
77
+ seed?: number
78
+ /** Camera motion type */
79
+ motion?: 'static' | 'pan' | 'zoom' | 'orbit' | 'dolly' | 'handheld'
80
+ /** Motion intensity (0-1) */
81
+ motionIntensity?: number
82
+ /** Whether to loop the video */
83
+ loop?: boolean
84
+ /** Additional model-specific parameters */
85
+ modelParams?: Record<string, unknown>
86
+ }
87
+
88
+ /**
89
+ * Metadata about the generated video
90
+ */
91
+ export interface VideoMetadata {
92
+ /** Model used for generation */
93
+ model: string
94
+ /** Video duration in seconds */
95
+ duration: number
96
+ /** Video resolution */
97
+ resolution: string
98
+ /** Frames per second */
99
+ fps: number
100
+ /** Aspect ratio */
101
+ aspectRatio: string
102
+ /** Generation time in milliseconds */
103
+ generationTime: number
104
+ /** File size in bytes (if available) */
105
+ fileSize?: number
106
+ /** Video format */
107
+ format?: string
108
+ /** Style applied */
109
+ style?: string
110
+ /** Seed used for generation */
111
+ seed?: number
112
+ /** Cost in credits/tokens (if applicable) */
113
+ cost?: number
114
+ }
115
+
116
+ /**
117
+ * Result of video generation
118
+ */
119
+ export interface VideoResult {
120
+ /** URL of the generated video */
121
+ url: string
122
+ /** Original prompt used */
123
+ prompt: string
124
+ /** Generation metadata */
125
+ metadata: VideoMetadata
126
+ /** Thumbnail URL (if available) */
127
+ thumbnail?: string
128
+ /** Preview GIF URL (if available) */
129
+ preview?: string
130
+ /** Status of generation */
131
+ status: 'completed' | 'processing' | 'failed'
132
+ /** Error message if failed */
133
+ error?: string
134
+ }
135
+
136
+ /**
137
+ * Options for video-from-image generation
138
+ */
139
+ export interface VideoFromImageOptions extends Omit<VideoOptions, 'prompt'> {
140
+ /** Source image URL */
141
+ imageUrl: string
142
+ /** Text prompt describing the desired motion/animation */
143
+ prompt: string
144
+ /** How much the image can change (0-1) */
145
+ imageFidelity?: number
146
+ }
147
+
148
+ /**
149
+ * Options for video extension
150
+ */
151
+ export interface VideoExtendOptions {
152
+ /** Source video URL */
153
+ videoUrl: string
154
+ /** Additional duration in seconds to add */
155
+ duration: number
156
+ /** Prompt for the extension (optional) */
157
+ prompt?: string
158
+ /** Direction to extend: beginning or end */
159
+ direction?: 'forward' | 'backward'
160
+ /** Overlap frames for smoother transition */
161
+ overlap?: number
162
+ }
163
+
164
+ /**
165
+ * Options for video editing
166
+ */
167
+ export interface VideoEditOptions {
168
+ /** Source video URL */
169
+ videoUrl: string
170
+ /** Edit prompt describing changes */
171
+ prompt: string
172
+ /** Specific region to edit (if supported) */
173
+ region?: {
174
+ x: number
175
+ y: number
176
+ width: number
177
+ height: number
178
+ }
179
+ /** Mask image URL for selective editing */
180
+ maskUrl?: string
181
+ /** Edit strength (0-1) */
182
+ strength?: number
183
+ /** Preserve audio from original */
184
+ preserveAudio?: boolean
185
+ }
186
+
187
+ // ============================================================================
188
+ // Helper Functions
189
+ // ============================================================================
190
+
191
+ /**
192
+ * Build metadata object, only including defined optional fields
193
+ */
194
+ function buildMetadata(
195
+ model: string,
196
+ duration: number,
197
+ resolution: string,
198
+ fps: number,
199
+ aspectRatio: string,
200
+ generationTime: number,
201
+ optionals: {
202
+ fileSize?: number
203
+ format?: string
204
+ style?: string
205
+ seed?: number
206
+ cost?: number
207
+ } = {}
208
+ ): VideoMetadata {
209
+ const metadata: VideoMetadata = {
210
+ model,
211
+ duration,
212
+ resolution,
213
+ fps,
214
+ aspectRatio,
215
+ generationTime,
216
+ }
217
+
218
+ if (optionals.fileSize !== undefined) metadata.fileSize = optionals.fileSize
219
+ if (optionals.format !== undefined) metadata.format = optionals.format
220
+ if (optionals.style !== undefined) metadata.style = optionals.style
221
+ if (optionals.seed !== undefined) metadata.seed = optionals.seed
222
+ if (optionals.cost !== undefined) metadata.cost = optionals.cost
223
+
224
+ return metadata
225
+ }
226
+
227
+ /**
228
+ * Resolve the worker URL for video generation based on model
229
+ */
230
+ function resolveVideoWorkerUrl(model: string): string {
231
+ // Map models to their respective worker endpoints
232
+ const workerMap: Record<string, string> = {
233
+ 'runway-gen3': 'https://video.workers.do/runway/gen3',
234
+ 'runway-gen2': 'https://video.workers.do/runway/gen2',
235
+ 'pika-1.0': 'https://video.workers.do/pika/1.0',
236
+ 'pika-1.5': 'https://video.workers.do/pika/1.5',
237
+ 'stable-video': 'https://video.workers.do/stability/svd',
238
+ minimax: 'https://video.workers.do/minimax',
239
+ kling: 'https://video.workers.do/kling',
240
+ luma: 'https://video.workers.do/luma',
241
+ }
242
+
243
+ // Check for custom endpoint in environment
244
+ const customEndpoint =
245
+ typeof globalThis !== 'undefined' && 'process' in globalThis
246
+ ? (globalThis as { process?: { env?: Record<string, string> } }).process?.env?.[
247
+ 'VIDEO_WORKER_URL'
248
+ ]
249
+ : undefined
250
+
251
+ if (customEndpoint) {
252
+ return customEndpoint
253
+ }
254
+
255
+ return workerMap[model] || `https://video.workers.do/${model}`
256
+ }
257
+
258
+ // ============================================================================
259
+ // Video Generation Function
260
+ // ============================================================================
261
+
262
+ /**
263
+ * Generate a video from a text prompt.
264
+ *
265
+ * Creates AI-generated video content using models like Runway, Pika,
266
+ * Stable Video, and others. Returns the video URL with rich metadata
267
+ * about the generation process.
268
+ *
269
+ * @param prompt - Text description of the video to generate
270
+ * @param options - Generation options (duration, fps, resolution, model, etc.)
271
+ * @returns Promise resolving to VideoResult with URL and metadata
272
+ *
273
+ * @example
274
+ * ```ts
275
+ * // Basic video generation
276
+ * const result = await video('A serene mountain lake at sunset with gentle ripples')
277
+ * console.log(result.url) // URL of generated video
278
+ * console.log(result.metadata.duration) // Video duration in seconds
279
+ * ```
280
+ *
281
+ * @example
282
+ * ```ts
283
+ * // With options
284
+ * const result = await video('A futuristic city with flying cars', {
285
+ * duration: 8,
286
+ * resolution: '4k',
287
+ * style: 'cinematic',
288
+ * model: 'runway-gen3',
289
+ * motion: 'dolly',
290
+ * })
291
+ * ```
292
+ *
293
+ * @example
294
+ * ```ts
295
+ * // With negative prompt
296
+ * const result = await video('A happy golden retriever playing in a park', {
297
+ * negativePrompt: 'blurry, distorted, extra limbs',
298
+ * guidance: 12,
299
+ * })
300
+ * ```
301
+ */
302
+ export async function video(
303
+ prompt: string,
304
+ options: Partial<VideoOptions> = {}
305
+ ): Promise<VideoResult> {
306
+ const {
307
+ duration = 4,
308
+ fps = 24,
309
+ resolution = '1080p',
310
+ aspectRatio = '16:9',
311
+ style,
312
+ model = 'runway-gen3',
313
+ negativePrompt,
314
+ guidance,
315
+ seed,
316
+ motion,
317
+ motionIntensity,
318
+ loop,
319
+ modelParams,
320
+ } = options
321
+
322
+ const startTime = Date.now()
323
+
324
+ // Build the worker URL for video generation
325
+ const workerUrl = resolveVideoWorkerUrl(model)
326
+
327
+ try {
328
+ const response = await fetch(workerUrl, {
329
+ method: 'POST',
330
+ headers: {
331
+ 'Content-Type': 'application/json',
332
+ },
333
+ body: JSON.stringify({
334
+ prompt,
335
+ duration,
336
+ fps,
337
+ resolution,
338
+ aspectRatio,
339
+ style,
340
+ model,
341
+ negativePrompt,
342
+ guidance,
343
+ seed,
344
+ motion,
345
+ motionIntensity,
346
+ loop,
347
+ ...modelParams,
348
+ }),
349
+ })
350
+
351
+ if (!response.ok) {
352
+ const errorText = await response.text()
353
+ throw new Error(`Video generation failed: ${response.status} - ${errorText}`)
354
+ }
355
+
356
+ const data = (await response.json()) as {
357
+ url: string
358
+ thumbnail?: string
359
+ preview?: string
360
+ fileSize?: number
361
+ format?: string
362
+ seed?: number
363
+ cost?: number
364
+ }
365
+
366
+ const optionals: {
367
+ fileSize?: number
368
+ format?: string
369
+ style?: string
370
+ seed?: number
371
+ cost?: number
372
+ } = {
373
+ format: data.format || 'mp4',
374
+ }
375
+
376
+ if (data.fileSize !== undefined) optionals.fileSize = data.fileSize
377
+ if (style !== undefined) optionals.style = style
378
+ if (data.seed !== undefined) optionals.seed = data.seed
379
+ else if (seed !== undefined) optionals.seed = seed
380
+ if (data.cost !== undefined) optionals.cost = data.cost
381
+
382
+ const result: VideoResult = {
383
+ url: data.url,
384
+ prompt,
385
+ metadata: buildMetadata(
386
+ model,
387
+ duration,
388
+ resolution,
389
+ fps,
390
+ aspectRatio,
391
+ Date.now() - startTime,
392
+ optionals
393
+ ),
394
+ status: 'completed',
395
+ }
396
+
397
+ if (data.thumbnail) result.thumbnail = data.thumbnail
398
+ if (data.preview) result.preview = data.preview
399
+
400
+ return result
401
+ } catch (error) {
402
+ const result: VideoResult = {
403
+ url: '',
404
+ prompt,
405
+ metadata: buildMetadata(
406
+ model,
407
+ duration,
408
+ resolution,
409
+ fps,
410
+ aspectRatio,
411
+ Date.now() - startTime
412
+ ),
413
+ status: 'failed',
414
+ error: error instanceof Error ? error.message : String(error),
415
+ }
416
+
417
+ return result
418
+ }
419
+ }
420
+
421
+ // ============================================================================
422
+ // Helper Methods
423
+ // ============================================================================
424
+
425
+ /**
426
+ * Generate a video from a static image.
427
+ *
428
+ * Animates a source image based on a motion prompt, bringing still
429
+ * images to life with AI-generated animation.
430
+ *
431
+ * @param imageUrl - URL of the source image
432
+ * @param prompt - Text description of the desired motion/animation
433
+ * @param options - Additional generation options
434
+ * @returns Promise resolving to VideoResult
435
+ *
436
+ * @example
437
+ * ```ts
438
+ * const result = await video.fromImage(
439
+ * 'https://example.com/landscape.jpg',
440
+ * 'Camera slowly pans across the scene, clouds drift by',
441
+ * { duration: 6, imageFidelity: 0.8 }
442
+ * )
443
+ * ```
444
+ */
445
+ video.fromImage = async function fromImage(
446
+ imageUrl: string,
447
+ prompt: string,
448
+ options: Partial<Omit<VideoFromImageOptions, 'imageUrl' | 'prompt'>> = {}
449
+ ): Promise<VideoResult> {
450
+ const {
451
+ duration = 4,
452
+ fps = 24,
453
+ resolution = '1080p',
454
+ aspectRatio = '16:9',
455
+ style,
456
+ model = 'runway-gen3',
457
+ imageFidelity = 0.7,
458
+ motion,
459
+ motionIntensity,
460
+ } = options
461
+
462
+ const startTime = Date.now()
463
+ const workerUrl = resolveVideoWorkerUrl(model)
464
+
465
+ try {
466
+ const response = await fetch(workerUrl, {
467
+ method: 'POST',
468
+ headers: {
469
+ 'Content-Type': 'application/json',
470
+ },
471
+ body: JSON.stringify({
472
+ mode: 'image-to-video',
473
+ imageUrl,
474
+ prompt,
475
+ duration,
476
+ fps,
477
+ resolution,
478
+ aspectRatio,
479
+ style,
480
+ model,
481
+ imageFidelity,
482
+ motion,
483
+ motionIntensity,
484
+ }),
485
+ })
486
+
487
+ if (!response.ok) {
488
+ const errorText = await response.text()
489
+ throw new Error(`Image-to-video generation failed: ${response.status} - ${errorText}`)
490
+ }
491
+
492
+ const data = (await response.json()) as {
493
+ url: string
494
+ thumbnail?: string
495
+ preview?: string
496
+ fileSize?: number
497
+ format?: string
498
+ seed?: number
499
+ cost?: number
500
+ }
501
+
502
+ const optionals: {
503
+ fileSize?: number
504
+ format?: string
505
+ style?: string
506
+ seed?: number
507
+ cost?: number
508
+ } = {
509
+ format: data.format || 'mp4',
510
+ }
511
+
512
+ if (data.fileSize !== undefined) optionals.fileSize = data.fileSize
513
+ if (style !== undefined) optionals.style = style
514
+ if (data.seed !== undefined) optionals.seed = data.seed
515
+ if (data.cost !== undefined) optionals.cost = data.cost
516
+
517
+ const result: VideoResult = {
518
+ url: data.url,
519
+ prompt,
520
+ metadata: buildMetadata(
521
+ model,
522
+ duration,
523
+ resolution,
524
+ fps,
525
+ aspectRatio,
526
+ Date.now() - startTime,
527
+ optionals
528
+ ),
529
+ status: 'completed',
530
+ }
531
+
532
+ if (data.thumbnail) result.thumbnail = data.thumbnail
533
+ if (data.preview) result.preview = data.preview
534
+
535
+ return result
536
+ } catch (error) {
537
+ return {
538
+ url: '',
539
+ prompt,
540
+ metadata: buildMetadata(
541
+ model,
542
+ duration,
543
+ resolution,
544
+ fps,
545
+ aspectRatio,
546
+ Date.now() - startTime
547
+ ),
548
+ status: 'failed',
549
+ error: error instanceof Error ? error.message : String(error),
550
+ }
551
+ }
552
+ }
553
+
554
+ /**
555
+ * Extend an existing video by generating additional frames.
556
+ *
557
+ * Continues a video seamlessly by generating new content that
558
+ * matches the style and content of the original.
559
+ *
560
+ * @param videoUrl - URL of the source video
561
+ * @param duration - Additional duration in seconds to add
562
+ * @param options - Extension options
563
+ * @returns Promise resolving to VideoResult with extended video
564
+ *
565
+ * @example
566
+ * ```ts
567
+ * // Extend video by 4 more seconds
568
+ * const result = await video.extend(
569
+ * 'https://example.com/short-clip.mp4',
570
+ * 4
571
+ * )
572
+ * ```
573
+ *
574
+ * @example
575
+ * ```ts
576
+ * // Extend with a prompt hint
577
+ * const result = await video.extend(
578
+ * 'https://example.com/scene.mp4',
579
+ * 6,
580
+ * { prompt: 'The character turns and walks away', direction: 'forward' }
581
+ * )
582
+ * ```
583
+ */
584
+ video.extend = async function extend(
585
+ videoUrl: string,
586
+ duration: number,
587
+ options: Partial<Omit<VideoExtendOptions, 'videoUrl' | 'duration'>> = {}
588
+ ): Promise<VideoResult> {
589
+ const { prompt, direction = 'forward', overlap = 4 } = options
590
+
591
+ const startTime = Date.now()
592
+ const workerUrl = resolveVideoWorkerUrl('runway-gen3')
593
+
594
+ try {
595
+ const response = await fetch(workerUrl, {
596
+ method: 'POST',
597
+ headers: {
598
+ 'Content-Type': 'application/json',
599
+ },
600
+ body: JSON.stringify({
601
+ mode: 'extend',
602
+ videoUrl,
603
+ duration,
604
+ prompt,
605
+ direction,
606
+ overlap,
607
+ }),
608
+ })
609
+
610
+ if (!response.ok) {
611
+ const errorText = await response.text()
612
+ throw new Error(`Video extension failed: ${response.status} - ${errorText}`)
613
+ }
614
+
615
+ const data = (await response.json()) as {
616
+ url: string
617
+ thumbnail?: string
618
+ preview?: string
619
+ fileSize?: number
620
+ format?: string
621
+ totalDuration?: number
622
+ cost?: number
623
+ }
624
+
625
+ const resultPrompt = prompt || 'Extended video'
626
+ const optionals: {
627
+ fileSize?: number
628
+ format?: string
629
+ style?: string
630
+ seed?: number
631
+ cost?: number
632
+ } = {
633
+ format: data.format || 'mp4',
634
+ }
635
+
636
+ if (data.fileSize !== undefined) optionals.fileSize = data.fileSize
637
+ if (data.cost !== undefined) optionals.cost = data.cost
638
+
639
+ const result: VideoResult = {
640
+ url: data.url,
641
+ prompt: resultPrompt,
642
+ metadata: buildMetadata(
643
+ 'runway-gen3',
644
+ data.totalDuration || duration,
645
+ '1080p',
646
+ 24,
647
+ '16:9',
648
+ Date.now() - startTime,
649
+ optionals
650
+ ),
651
+ status: 'completed',
652
+ }
653
+
654
+ if (data.thumbnail) result.thumbnail = data.thumbnail
655
+ if (data.preview) result.preview = data.preview
656
+
657
+ return result
658
+ } catch (error) {
659
+ return {
660
+ url: '',
661
+ prompt: prompt || 'Extended video',
662
+ metadata: buildMetadata('runway-gen3', duration, '1080p', 24, '16:9', Date.now() - startTime),
663
+ status: 'failed',
664
+ error: error instanceof Error ? error.message : String(error),
665
+ }
666
+ }
667
+ }
668
+
669
+ /**
670
+ * Edit an existing video using AI.
671
+ *
672
+ * Modifies a video based on a text prompt, allowing for
673
+ * style changes, object removal/addition, and other edits.
674
+ *
675
+ * @param videoUrl - URL of the video to edit
676
+ * @param prompt - Text description of the desired edits
677
+ * @param options - Edit options
678
+ * @returns Promise resolving to VideoResult with edited video
679
+ *
680
+ * @example
681
+ * ```ts
682
+ * // Change the style of a video
683
+ * const result = await video.edit(
684
+ * 'https://example.com/original.mp4',
685
+ * 'Make it look like a vintage 1970s film'
686
+ * )
687
+ * ```
688
+ *
689
+ * @example
690
+ * ```ts
691
+ * // Edit with mask
692
+ * const result = await video.edit(
693
+ * 'https://example.com/scene.mp4',
694
+ * 'Replace the car with a spaceship',
695
+ * { maskUrl: 'https://example.com/car-mask.png', strength: 0.9 }
696
+ * )
697
+ * ```
698
+ */
699
+ video.edit = async function edit(
700
+ videoUrl: string,
701
+ prompt: string,
702
+ options: Partial<Omit<VideoEditOptions, 'videoUrl' | 'prompt'>> = {}
703
+ ): Promise<VideoResult> {
704
+ const { region, maskUrl, strength = 0.7, preserveAudio = true } = options
705
+
706
+ const startTime = Date.now()
707
+ const workerUrl = resolveVideoWorkerUrl('runway-gen3')
708
+
709
+ try {
710
+ const response = await fetch(workerUrl, {
711
+ method: 'POST',
712
+ headers: {
713
+ 'Content-Type': 'application/json',
714
+ },
715
+ body: JSON.stringify({
716
+ mode: 'edit',
717
+ videoUrl,
718
+ prompt,
719
+ region,
720
+ maskUrl,
721
+ strength,
722
+ preserveAudio,
723
+ }),
724
+ })
725
+
726
+ if (!response.ok) {
727
+ const errorText = await response.text()
728
+ throw new Error(`Video editing failed: ${response.status} - ${errorText}`)
729
+ }
730
+
731
+ const data = (await response.json()) as {
732
+ url: string
733
+ thumbnail?: string
734
+ preview?: string
735
+ fileSize?: number
736
+ format?: string
737
+ duration?: number
738
+ cost?: number
739
+ }
740
+
741
+ const optionals: {
742
+ fileSize?: number
743
+ format?: string
744
+ style?: string
745
+ seed?: number
746
+ cost?: number
747
+ } = {
748
+ format: data.format || 'mp4',
749
+ }
750
+
751
+ if (data.fileSize !== undefined) optionals.fileSize = data.fileSize
752
+ if (data.cost !== undefined) optionals.cost = data.cost
753
+
754
+ const result: VideoResult = {
755
+ url: data.url,
756
+ prompt,
757
+ metadata: buildMetadata(
758
+ 'runway-gen3',
759
+ data.duration || 4,
760
+ '1080p',
761
+ 24,
762
+ '16:9',
763
+ Date.now() - startTime,
764
+ optionals
765
+ ),
766
+ status: 'completed',
767
+ }
768
+
769
+ if (data.thumbnail) result.thumbnail = data.thumbnail
770
+ if (data.preview) result.preview = data.preview
771
+
772
+ return result
773
+ } catch (error) {
774
+ return {
775
+ url: '',
776
+ prompt,
777
+ metadata: buildMetadata('runway-gen3', 4, '1080p', 24, '16:9', Date.now() - startTime),
778
+ status: 'failed',
779
+ error: error instanceof Error ? error.message : String(error),
780
+ }
781
+ }
782
+ }
783
+
784
+ /**
785
+ * Create a video generator with a preset style.
786
+ *
787
+ * Returns a function that generates videos with the specified
788
+ * style pre-applied. Useful for maintaining consistent aesthetics.
789
+ *
790
+ * @param style - Style preset to apply to all generated videos
791
+ * @returns Function that generates styled videos
792
+ *
793
+ * @example
794
+ * ```ts
795
+ * // Create a cinematic video generator
796
+ * const cinematicVideo = video.style('cinematic')
797
+ *
798
+ * const result1 = await cinematicVideo('A dramatic sunset over the ocean')
799
+ * const result2 = await cinematicVideo('A hero walking into the distance')
800
+ * // Both will have cinematic style applied
801
+ * ```
802
+ *
803
+ * @example
804
+ * ```ts
805
+ * // Chain with other options
806
+ * const animeVideo = video.style('anime')
807
+ * const result = await animeVideo('A magical girl transformation sequence', {
808
+ * duration: 8,
809
+ * resolution: '4k',
810
+ * })
811
+ * ```
812
+ */
813
+ video.style = function style(stylePreset: VideoStyle) {
814
+ return function styledVideo(
815
+ prompt: string,
816
+ options: Partial<Omit<VideoOptions, 'style'>> = {}
817
+ ): Promise<VideoResult> {
818
+ return video(prompt, { ...options, style: stylePreset })
819
+ }
820
+ }
821
+
822
+ /**
823
+ * Generate multiple video variations from the same prompt.
824
+ *
825
+ * @param prompt - Text description of the video
826
+ * @param count - Number of variations to generate
827
+ * @param options - Generation options
828
+ * @returns Promise resolving to array of VideoResults
829
+ *
830
+ * @example
831
+ * ```ts
832
+ * const variations = await video.variations(
833
+ * 'A cat playing with a ball of yarn',
834
+ * 3,
835
+ * { duration: 4 }
836
+ * )
837
+ *
838
+ * variations.forEach((v, i) => {
839
+ * console.log(`Variation ${i + 1}: ${v.url}`)
840
+ * })
841
+ * ```
842
+ */
843
+ video.variations = async function variations(
844
+ prompt: string,
845
+ count: number,
846
+ options: Partial<VideoOptions> = {}
847
+ ): Promise<VideoResult[]> {
848
+ const results = await Promise.all(
849
+ Array.from({ length: count }, (_, i) =>
850
+ video(prompt, {
851
+ ...options,
852
+ // Use different seeds for variations
853
+ seed:
854
+ options.seed !== undefined ? options.seed + i : Math.floor(Math.random() * 1000000) + i,
855
+ })
856
+ )
857
+ )
858
+ return results
859
+ }
860
+
861
+ /**
862
+ * Generate video with specific camera motion.
863
+ *
864
+ * @param prompt - Text description of the scene
865
+ * @param motion - Camera motion type
866
+ * @param options - Additional generation options
867
+ * @returns Promise resolving to VideoResult
868
+ *
869
+ * @example
870
+ * ```ts
871
+ * const result = await video.withMotion(
872
+ * 'A beautiful landscape',
873
+ * 'dolly',
874
+ * { motionIntensity: 0.5 }
875
+ * )
876
+ * ```
877
+ */
878
+ video.withMotion = async function withMotion(
879
+ prompt: string,
880
+ motion: 'static' | 'pan' | 'zoom' | 'orbit' | 'dolly' | 'handheld',
881
+ options: Partial<Omit<VideoOptions, 'motion'>> = {}
882
+ ): Promise<VideoResult> {
883
+ return video(prompt, { ...options, motion })
884
+ }
885
+
886
+ /**
887
+ * Generate a looping video (perfect for backgrounds, GIFs).
888
+ *
889
+ * @param prompt - Text description of the video
890
+ * @param options - Generation options
891
+ * @returns Promise resolving to VideoResult with looping video
892
+ *
893
+ * @example
894
+ * ```ts
895
+ * const result = await video.loop(
896
+ * 'Gently swaying grass in the wind',
897
+ * { duration: 3 }
898
+ * )
899
+ * ```
900
+ */
901
+ video.loop = async function loop(
902
+ prompt: string,
903
+ options: Partial<Omit<VideoOptions, 'loop'>> = {}
904
+ ): Promise<VideoResult> {
905
+ return video(prompt, { ...options, loop: true })
906
+ }