digital-workers 2.1.1 → 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.
- package/CHANGELOG.md +23 -0
- package/README.md +136 -180
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +34 -21
- package/dist/actions.js.map +1 -1
- package/dist/agent-comms.d.ts +438 -0
- package/dist/agent-comms.d.ts.map +1 -0
- package/dist/agent-comms.js +677 -0
- package/dist/agent-comms.js.map +1 -0
- package/dist/approve.d.ts +40 -8
- package/dist/approve.d.ts.map +1 -1
- package/dist/approve.js +86 -20
- package/dist/approve.js.map +1 -1
- package/dist/ask.d.ts +38 -7
- package/dist/ask.d.ts.map +1 -1
- package/dist/ask.js +85 -25
- package/dist/ask.js.map +1 -1
- package/dist/browse.d.ts +223 -0
- package/dist/browse.d.ts.map +1 -0
- package/dist/browse.js +392 -0
- package/dist/browse.js.map +1 -0
- package/dist/capability-tiers.d.ts +230 -0
- package/dist/capability-tiers.d.ts.map +1 -0
- package/dist/capability-tiers.js +388 -0
- package/dist/capability-tiers.js.map +1 -0
- package/dist/cascade-context.d.ts +523 -0
- package/dist/cascade-context.d.ts.map +1 -0
- package/dist/cascade-context.js +494 -0
- package/dist/cascade-context.js.map +1 -0
- package/dist/client.d.ts +162 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +64 -0
- package/dist/client.js.map +1 -0
- package/dist/decide.d.ts +42 -6
- package/dist/decide.d.ts.map +1 -1
- package/dist/decide.js +54 -11
- package/dist/decide.js.map +1 -1
- package/dist/do.d.ts +36 -7
- package/dist/do.d.ts.map +1 -1
- package/dist/do.js +82 -39
- package/dist/do.js.map +1 -1
- package/dist/error-escalation.d.ts +416 -0
- package/dist/error-escalation.d.ts.map +1 -0
- package/dist/error-escalation.js +656 -0
- package/dist/error-escalation.js.map +1 -0
- package/dist/generate.d.ts +48 -7
- package/dist/generate.d.ts.map +1 -1
- package/dist/generate.js +49 -8
- package/dist/generate.js.map +1 -1
- package/dist/goals.d.ts +10 -9
- package/dist/goals.d.ts.map +1 -1
- package/dist/goals.js +30 -24
- package/dist/goals.js.map +1 -1
- package/dist/image.d.ts +189 -0
- package/dist/image.d.ts.map +1 -0
- package/dist/image.js +528 -0
- package/dist/image.js.map +1 -0
- package/dist/index.d.ts +59 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +92 -2
- package/dist/index.js.map +1 -1
- package/dist/is.d.ts +45 -10
- package/dist/is.d.ts.map +1 -1
- package/dist/is.js +56 -21
- package/dist/is.js.map +1 -1
- package/dist/kpis.d.ts +24 -15
- package/dist/kpis.d.ts.map +1 -1
- package/dist/kpis.js +16 -14
- package/dist/kpis.js.map +1 -1
- package/dist/load-balancing.d.ts +395 -0
- package/dist/load-balancing.d.ts.map +1 -0
- package/dist/load-balancing.js +991 -0
- package/dist/load-balancing.js.map +1 -0
- package/dist/logger.d.ts +76 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +39 -0
- package/dist/logger.js.map +1 -0
- package/dist/notify.d.ts +38 -9
- package/dist/notify.d.ts.map +1 -1
- package/dist/notify.js +72 -17
- package/dist/notify.js.map +1 -1
- package/dist/role.d.ts +5 -4
- package/dist/role.d.ts.map +1 -1
- package/dist/role.js +13 -10
- package/dist/role.js.map +1 -1
- package/dist/runtime.d.ts +310 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +510 -0
- package/dist/runtime.js.map +1 -0
- package/dist/team.d.ts +11 -6
- package/dist/team.d.ts.map +1 -1
- package/dist/team.js +22 -15
- package/dist/team.js.map +1 -1
- package/dist/transports/email.d.ts +318 -0
- package/dist/transports/email.d.ts.map +1 -0
- package/dist/transports/email.js +779 -0
- package/dist/transports/email.js.map +1 -0
- package/dist/transports/slack.d.ts +515 -0
- package/dist/transports/slack.d.ts.map +1 -0
- package/dist/transports/slack.js +844 -0
- package/dist/transports/slack.js.map +1 -0
- package/dist/transports.d.ts.map +1 -1
- package/dist/transports.js +44 -25
- package/dist/transports.js.map +1 -1
- package/dist/types.d.ts +149 -19
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -1
- package/dist/utils/id.d.ts +19 -0
- package/dist/utils/id.d.ts.map +1 -0
- package/dist/utils/id.js +21 -0
- package/dist/utils/id.js.map +1 -0
- package/dist/video.d.ts +203 -0
- package/dist/video.d.ts.map +1 -0
- package/dist/video.js +528 -0
- package/dist/video.js.map +1 -0
- package/dist/worker.d.ts +343 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +698 -0
- package/dist/worker.js.map +1 -0
- package/package.json +24 -5
- package/src/actions.ts +48 -38
- package/src/agent-comms.ts +1200 -0
- package/src/approve.ts +91 -20
- package/src/ask.ts +99 -25
- package/src/browse.ts +627 -0
- package/src/capability-tiers.ts +545 -0
- package/src/cascade-context.ts +648 -0
- package/src/client.ts +221 -0
- package/src/decide.ts +81 -35
- package/src/do.ts +98 -52
- package/src/error-escalation.ts +1123 -0
- package/src/generate.ts +52 -18
- package/src/goals.ts +36 -27
- package/src/image.ts +816 -0
- package/src/index.ts +410 -2
- package/src/is.ts +59 -25
- package/src/kpis.ts +41 -36
- package/src/load-balancing.ts +1467 -0
- package/src/logger.ts +93 -0
- package/src/notify.ts +78 -17
- package/src/role.ts +30 -20
- package/src/runtime.ts +796 -0
- package/src/team.ts +24 -19
- package/src/transports/email.ts +1160 -0
- package/src/transports/slack.ts +1320 -0
- package/src/transports.ts +58 -43
- package/src/types.ts +182 -46
- package/src/utils/id.ts +21 -0
- package/src/video.ts +906 -0
- package/src/worker.ts +1007 -0
- package/test/agent-comms.test.ts +1397 -0
- package/test/approve.test.ts +305 -0
- package/test/ask.test.ts +274 -0
- package/test/browse.test.ts +361 -0
- package/test/capability-tiers.test.ts +631 -0
- package/test/cascade-context.test.ts +692 -0
- package/test/decide.test.ts +252 -0
- package/test/do.test.ts +144 -0
- package/test/error-escalation.test.ts +1205 -0
- package/test/error-logging.test.ts +357 -0
- package/test/generate.test.ts +319 -0
- package/test/image.test.ts +398 -0
- package/test/is.test.ts +287 -0
- package/test/load-balancing-safety.test.ts +404 -0
- package/test/load-balancing-thread-safety.test.ts +464 -0
- package/test/load-balancing.test.ts +1145 -0
- package/test/notify.test.ts +434 -0
- package/test/primitives.test.ts +320 -0
- package/test/runtime-integration.test.ts +892 -0
- package/test/transports/crypto.test.ts +230 -0
- package/test/transports/email.test.ts +866 -0
- package/test/transports/id-generation.test.ts +91 -0
- package/test/transports/slack.test.ts +760 -0
- package/test/type-safety.test.ts +834 -0
- package/test/types.test.ts +95 -2
- package/test/video.test.ts +530 -0
- package/test/worker.test.ts +1433 -0
- package/tsconfig.json +4 -1
- package/vitest.config.ts +42 -0
- package/wrangler.jsonc +36 -0
- package/.turbo/turbo-build.log +0 -5
- package/src/actions.js +0 -436
- package/src/approve.js +0 -234
- package/src/ask.js +0 -226
- package/src/decide.js +0 -244
- package/src/do.js +0 -227
- package/src/generate.js +0 -298
- package/src/goals.js +0 -205
- package/src/index.js +0 -68
- package/src/is.js +0 -317
- package/src/kpis.js +0 -270
- package/src/notify.js +0 -219
- package/src/role.js +0 -110
- package/src/team.js +0 -130
- package/src/transports.js +0 -357
- package/src/types.js +0 -71
package/src/image.ts
ADDED
|
@@ -0,0 +1,816 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image generation functionality for digital workers
|
|
3
|
+
*
|
|
4
|
+
* This module provides image generation primitives within a worker context,
|
|
5
|
+
* with rich metadata about the generation process.
|
|
6
|
+
*
|
|
7
|
+
* - `image()` - Generates images with full metadata (model, size, style)
|
|
8
|
+
* - `image.variations()` - Creates variations of an existing image
|
|
9
|
+
* - `image.edit()` - Edits an image with a text prompt and optional mask
|
|
10
|
+
* - `image.upscale()` - Upscales an image to higher resolution
|
|
11
|
+
* - `image.style()` - Creates a curried function for a specific style
|
|
12
|
+
*
|
|
13
|
+
* The key difference from direct API calls is context and metadata:
|
|
14
|
+
* - digital-workers returns `ImageResult` with content + metadata
|
|
15
|
+
* - Direct API calls return just the generated image
|
|
16
|
+
*
|
|
17
|
+
* @module
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Available image style presets
|
|
22
|
+
*/
|
|
23
|
+
export type ImageStyle =
|
|
24
|
+
| 'realistic'
|
|
25
|
+
| 'artistic'
|
|
26
|
+
| 'cartoon'
|
|
27
|
+
| 'abstract'
|
|
28
|
+
| 'photographic'
|
|
29
|
+
| 'digital-art'
|
|
30
|
+
| 'cinematic'
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Available image sizes
|
|
34
|
+
*/
|
|
35
|
+
export type ImageSize = '256x256' | '512x512' | '1024x1024' | '1024x1792' | '1792x1024'
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Available image formats
|
|
39
|
+
*/
|
|
40
|
+
export type ImageFormat = 'png' | 'jpeg' | 'webp' | 'b64_json' | 'url'
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Options for image generation
|
|
44
|
+
*/
|
|
45
|
+
export interface ImageOptions {
|
|
46
|
+
/** The prompt describing the image to generate */
|
|
47
|
+
prompt: string
|
|
48
|
+
/** The style preset to apply */
|
|
49
|
+
style?: ImageStyle
|
|
50
|
+
/** The size of the generated image */
|
|
51
|
+
size?: ImageSize
|
|
52
|
+
/** The model to use for generation */
|
|
53
|
+
model?: string
|
|
54
|
+
/** Number of images to generate (1-10) */
|
|
55
|
+
n?: number
|
|
56
|
+
/** Output format */
|
|
57
|
+
format?: ImageFormat
|
|
58
|
+
/** Quality setting */
|
|
59
|
+
quality?: 'standard' | 'hd'
|
|
60
|
+
/** Negative prompt - what to avoid in the image */
|
|
61
|
+
negativePrompt?: string
|
|
62
|
+
/** Seed for reproducible generation */
|
|
63
|
+
seed?: number
|
|
64
|
+
/** Additional model-specific parameters */
|
|
65
|
+
parameters?: Record<string, unknown>
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Result of image generation
|
|
70
|
+
*/
|
|
71
|
+
export interface ImageResult {
|
|
72
|
+
/** URL or base64 data of the generated image */
|
|
73
|
+
url: string
|
|
74
|
+
/** The original prompt */
|
|
75
|
+
prompt: string
|
|
76
|
+
/** The revised/enhanced prompt if the model modified it */
|
|
77
|
+
revisedPrompt?: string
|
|
78
|
+
/** Generation metadata */
|
|
79
|
+
metadata: {
|
|
80
|
+
/** Model used for generation */
|
|
81
|
+
model: string
|
|
82
|
+
/** Size of the generated image */
|
|
83
|
+
size: string
|
|
84
|
+
/** Style applied */
|
|
85
|
+
style?: string
|
|
86
|
+
/** Generation duration in milliseconds */
|
|
87
|
+
duration?: number
|
|
88
|
+
/** Seed used for generation */
|
|
89
|
+
seed?: number
|
|
90
|
+
/** Format of the output */
|
|
91
|
+
format?: ImageFormat
|
|
92
|
+
/** Quality setting used */
|
|
93
|
+
quality?: string
|
|
94
|
+
/** Provider-specific metadata */
|
|
95
|
+
provider?: Record<string, unknown>
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Options for image variations
|
|
101
|
+
*/
|
|
102
|
+
export interface VariationOptions {
|
|
103
|
+
/** Number of variations to generate */
|
|
104
|
+
count?: number
|
|
105
|
+
/** Size of the generated variations */
|
|
106
|
+
size?: ImageSize
|
|
107
|
+
/** Model to use for generation */
|
|
108
|
+
model?: string
|
|
109
|
+
/** Output format */
|
|
110
|
+
format?: ImageFormat
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Options for image editing
|
|
115
|
+
*/
|
|
116
|
+
export interface EditOptions {
|
|
117
|
+
/** The prompt describing the edit */
|
|
118
|
+
prompt: string
|
|
119
|
+
/** URL or base64 of the mask image (transparent areas will be edited) */
|
|
120
|
+
mask?: string
|
|
121
|
+
/** Size of the output image */
|
|
122
|
+
size?: ImageSize
|
|
123
|
+
/** Model to use for editing */
|
|
124
|
+
model?: string
|
|
125
|
+
/** Number of edits to generate */
|
|
126
|
+
n?: number
|
|
127
|
+
/** Output format */
|
|
128
|
+
format?: ImageFormat
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Options for image upscaling
|
|
133
|
+
*/
|
|
134
|
+
export interface UpscaleOptions {
|
|
135
|
+
/** Scale factor (2, 4, etc.) */
|
|
136
|
+
scale?: number
|
|
137
|
+
/** Model to use for upscaling */
|
|
138
|
+
model?: string
|
|
139
|
+
/** Output format */
|
|
140
|
+
format?: ImageFormat
|
|
141
|
+
/** Denoise level (0-1) */
|
|
142
|
+
denoise?: number
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Result of image upscaling
|
|
147
|
+
*/
|
|
148
|
+
export interface UpscaleResult extends ImageResult {
|
|
149
|
+
/** Original image dimensions */
|
|
150
|
+
originalSize: {
|
|
151
|
+
width: number
|
|
152
|
+
height: number
|
|
153
|
+
}
|
|
154
|
+
/** Upscaled image dimensions */
|
|
155
|
+
upscaledSize: {
|
|
156
|
+
width: number
|
|
157
|
+
height: number
|
|
158
|
+
}
|
|
159
|
+
/** Scale factor used */
|
|
160
|
+
scaleFactor: number
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Style preset configurations
|
|
165
|
+
*/
|
|
166
|
+
const STYLE_PROMPTS: Record<ImageStyle, string> = {
|
|
167
|
+
realistic: 'photorealistic, highly detailed, natural lighting, 8k resolution',
|
|
168
|
+
artistic: 'artistic interpretation, painterly style, creative composition',
|
|
169
|
+
cartoon: 'cartoon style, cel-shaded, vibrant colors, animated look',
|
|
170
|
+
abstract: 'abstract art, non-representational, geometric shapes, modern art',
|
|
171
|
+
photographic: 'professional photography, DSLR quality, sharp focus, studio lighting',
|
|
172
|
+
'digital-art': 'digital art, concept art, detailed illustration, artstation trending',
|
|
173
|
+
cinematic: 'cinematic composition, dramatic lighting, movie scene, film grain',
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Default configuration
|
|
178
|
+
*/
|
|
179
|
+
const DEFAULTS = {
|
|
180
|
+
model: 'dall-e-3',
|
|
181
|
+
size: '1024x1024' as ImageSize,
|
|
182
|
+
format: 'url' as ImageFormat,
|
|
183
|
+
quality: 'standard' as const,
|
|
184
|
+
n: 1,
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Declare process for environments where it exists (Node.js)
|
|
188
|
+
declare const process: { env?: Record<string, string | undefined> } | undefined
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Get environment variable safely (works in both Node.js and Workers)
|
|
192
|
+
*/
|
|
193
|
+
function getEnv(key: string): string | undefined {
|
|
194
|
+
if (typeof process !== 'undefined' && process?.env) {
|
|
195
|
+
return process.env[key]
|
|
196
|
+
}
|
|
197
|
+
return undefined
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Get the image generation endpoint based on model
|
|
202
|
+
*/
|
|
203
|
+
function getImageEndpoint(model: string): string {
|
|
204
|
+
// Route to appropriate worker based on model
|
|
205
|
+
const baseUrl =
|
|
206
|
+
getEnv('AI_GATEWAY_URL') || getEnv('IMAGE_GATEWAY_URL') || 'https://image.workers.do'
|
|
207
|
+
|
|
208
|
+
if (model.startsWith('dall-e') || model.startsWith('openai/')) {
|
|
209
|
+
return `${baseUrl}/openai/images/generations`
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (model.startsWith('stable-diffusion') || model.startsWith('stability/')) {
|
|
213
|
+
return `${baseUrl}/stability/images/generations`
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (model.startsWith('flux') || model.startsWith('black-forest/')) {
|
|
217
|
+
return `${baseUrl}/flux/images/generations`
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (model.startsWith('midjourney/')) {
|
|
221
|
+
return `${baseUrl}/midjourney/images/generations`
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Default to generic endpoint
|
|
225
|
+
return `${baseUrl}/images/generations`
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Get authorization headers
|
|
230
|
+
*/
|
|
231
|
+
function getAuthHeaders(): Record<string, string> {
|
|
232
|
+
const headers: Record<string, string> = {}
|
|
233
|
+
|
|
234
|
+
const openaiKey = getEnv('OPENAI_API_KEY')
|
|
235
|
+
if (openaiKey) {
|
|
236
|
+
headers['Authorization'] = `Bearer ${openaiKey}`
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const gatewayToken = getEnv('AI_GATEWAY_TOKEN')
|
|
240
|
+
if (gatewayToken) {
|
|
241
|
+
headers['X-Gateway-Token'] = gatewayToken
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return headers
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Enhance prompt with style modifiers
|
|
249
|
+
*/
|
|
250
|
+
function enhancePrompt(prompt: string, style?: ImageStyle): string {
|
|
251
|
+
if (!style || !STYLE_PROMPTS[style]) {
|
|
252
|
+
return prompt
|
|
253
|
+
}
|
|
254
|
+
return `${prompt}, ${STYLE_PROMPTS[style]}`
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Convert base64 data URL to Blob
|
|
259
|
+
*/
|
|
260
|
+
function base64ToBlob(dataUrl: string): Blob {
|
|
261
|
+
const base64Data = dataUrl.split(',')[1]
|
|
262
|
+
if (!base64Data) {
|
|
263
|
+
throw new Error('Invalid data URL format')
|
|
264
|
+
}
|
|
265
|
+
const binaryData = atob(base64Data)
|
|
266
|
+
const bytes = new Uint8Array(binaryData.length)
|
|
267
|
+
for (let i = 0; i < binaryData.length; i++) {
|
|
268
|
+
bytes[i] = binaryData.charCodeAt(i)
|
|
269
|
+
}
|
|
270
|
+
return new Blob([bytes], { type: 'image/png' })
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Generate an image from a text prompt
|
|
275
|
+
*
|
|
276
|
+
* @param prompt - The text prompt describing the image to generate
|
|
277
|
+
* @param options - Generation options (style, size, model, etc.)
|
|
278
|
+
* @returns Promise resolving to ImageResult with URL and metadata
|
|
279
|
+
*
|
|
280
|
+
* @example
|
|
281
|
+
* ```ts
|
|
282
|
+
* // Generate a simple image
|
|
283
|
+
* const result = await image('A sunset over mountains')
|
|
284
|
+
* console.log(result.url) // URL to the generated image
|
|
285
|
+
* console.log(result.metadata.model) // Model used
|
|
286
|
+
* ```
|
|
287
|
+
*
|
|
288
|
+
* @example
|
|
289
|
+
* ```ts
|
|
290
|
+
* // Generate with options
|
|
291
|
+
* const result = await image('A portrait of a robot', {
|
|
292
|
+
* style: 'cinematic',
|
|
293
|
+
* size: '1024x1024',
|
|
294
|
+
* quality: 'hd',
|
|
295
|
+
* })
|
|
296
|
+
* ```
|
|
297
|
+
*
|
|
298
|
+
* @example
|
|
299
|
+
* ```ts
|
|
300
|
+
* // Generate multiple images
|
|
301
|
+
* const result = await image('Abstract patterns', {
|
|
302
|
+
* style: 'abstract',
|
|
303
|
+
* n: 4,
|
|
304
|
+
* })
|
|
305
|
+
* ```
|
|
306
|
+
*/
|
|
307
|
+
export async function image(
|
|
308
|
+
prompt: string,
|
|
309
|
+
options: Partial<ImageOptions> = {}
|
|
310
|
+
): Promise<ImageResult> {
|
|
311
|
+
const startTime = Date.now()
|
|
312
|
+
|
|
313
|
+
const {
|
|
314
|
+
style,
|
|
315
|
+
size = DEFAULTS.size,
|
|
316
|
+
model = DEFAULTS.model,
|
|
317
|
+
n = DEFAULTS.n,
|
|
318
|
+
format = DEFAULTS.format,
|
|
319
|
+
quality = DEFAULTS.quality,
|
|
320
|
+
negativePrompt,
|
|
321
|
+
seed,
|
|
322
|
+
parameters = {},
|
|
323
|
+
} = options
|
|
324
|
+
|
|
325
|
+
const enhancedPrompt = enhancePrompt(prompt, style)
|
|
326
|
+
const endpoint = getImageEndpoint(model)
|
|
327
|
+
|
|
328
|
+
// Build the request payload
|
|
329
|
+
const payload: Record<string, unknown> = {
|
|
330
|
+
prompt: enhancedPrompt,
|
|
331
|
+
size,
|
|
332
|
+
n,
|
|
333
|
+
response_format: format === 'url' ? 'url' : format === 'b64_json' ? 'b64_json' : 'url',
|
|
334
|
+
...parameters,
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Add optional parameters using bracket notation for index signatures
|
|
338
|
+
if (quality && model.includes('dall-e-3')) {
|
|
339
|
+
payload['quality'] = quality
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (negativePrompt) {
|
|
343
|
+
payload['negative_prompt'] = negativePrompt
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (seed !== undefined) {
|
|
347
|
+
payload['seed'] = seed
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (model) {
|
|
351
|
+
payload['model'] = model
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Make the API request
|
|
355
|
+
const response = await fetch(endpoint, {
|
|
356
|
+
method: 'POST',
|
|
357
|
+
headers: {
|
|
358
|
+
'Content-Type': 'application/json',
|
|
359
|
+
...getAuthHeaders(),
|
|
360
|
+
},
|
|
361
|
+
body: JSON.stringify(payload),
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
if (!response.ok) {
|
|
365
|
+
const error = await response.text()
|
|
366
|
+
throw new Error(`Image generation failed: ${response.status} - ${error}`)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const apiResponse = (await response.json()) as {
|
|
370
|
+
data?: Array<{
|
|
371
|
+
url?: string
|
|
372
|
+
b64_json?: string
|
|
373
|
+
revised_prompt?: string
|
|
374
|
+
}>
|
|
375
|
+
created?: number
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const imageData = apiResponse.data?.[0]
|
|
379
|
+
|
|
380
|
+
if (!imageData) {
|
|
381
|
+
throw new Error('No image data returned from API')
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const imageUrl =
|
|
385
|
+
imageData.url || (imageData.b64_json ? `data:image/png;base64,${imageData.b64_json}` : '')
|
|
386
|
+
|
|
387
|
+
if (!imageUrl) {
|
|
388
|
+
throw new Error('No image URL or data returned from API')
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Build metadata, only including defined values
|
|
392
|
+
const metadata: ImageResult['metadata'] = {
|
|
393
|
+
model,
|
|
394
|
+
size,
|
|
395
|
+
duration: Date.now() - startTime,
|
|
396
|
+
format,
|
|
397
|
+
quality,
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (style) {
|
|
401
|
+
metadata.style = style
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (seed !== undefined) {
|
|
405
|
+
metadata.seed = seed
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const imageResult: ImageResult = {
|
|
409
|
+
url: imageUrl,
|
|
410
|
+
prompt,
|
|
411
|
+
metadata,
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Only add revisedPrompt if it's defined (exactOptionalPropertyTypes)
|
|
415
|
+
if (imageData.revised_prompt) {
|
|
416
|
+
imageResult.revisedPrompt = imageData.revised_prompt
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return imageResult
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Generate variations of an existing image
|
|
424
|
+
*
|
|
425
|
+
* @param imageUrl - URL or base64 of the source image
|
|
426
|
+
* @param options - Variation options
|
|
427
|
+
* @returns Promise resolving to array of ImageResults
|
|
428
|
+
*
|
|
429
|
+
* @example
|
|
430
|
+
* ```ts
|
|
431
|
+
* const variations = await image.variations('https://example.com/image.png', {
|
|
432
|
+
* count: 3,
|
|
433
|
+
* size: '1024x1024',
|
|
434
|
+
* })
|
|
435
|
+
*
|
|
436
|
+
* variations.forEach((v, i) => {
|
|
437
|
+
* console.log(`Variation ${i + 1}: ${v.url}`)
|
|
438
|
+
* })
|
|
439
|
+
* ```
|
|
440
|
+
*/
|
|
441
|
+
image.variations = async function variations(
|
|
442
|
+
imageUrl: string,
|
|
443
|
+
options: VariationOptions = {}
|
|
444
|
+
): Promise<ImageResult[]> {
|
|
445
|
+
const startTime = Date.now()
|
|
446
|
+
|
|
447
|
+
const {
|
|
448
|
+
count = 3,
|
|
449
|
+
size = DEFAULTS.size,
|
|
450
|
+
model = 'dall-e-2', // DALL-E 2 supports variations
|
|
451
|
+
format = DEFAULTS.format,
|
|
452
|
+
} = options
|
|
453
|
+
|
|
454
|
+
const baseUrl =
|
|
455
|
+
getEnv('AI_GATEWAY_URL') || getEnv('IMAGE_GATEWAY_URL') || 'https://image.workers.do'
|
|
456
|
+
const endpoint = `${baseUrl}/openai/images/variations`
|
|
457
|
+
|
|
458
|
+
// Build form data for image upload
|
|
459
|
+
const formData = new FormData()
|
|
460
|
+
|
|
461
|
+
// If it's a URL, we need to fetch the image first
|
|
462
|
+
if (imageUrl.startsWith('http')) {
|
|
463
|
+
const imageResponse = await fetch(imageUrl)
|
|
464
|
+
const imageBlob = await imageResponse.blob()
|
|
465
|
+
formData.append('image', imageBlob, 'image.png')
|
|
466
|
+
} else if (imageUrl.startsWith('data:')) {
|
|
467
|
+
// Handle base64 data URL
|
|
468
|
+
const blob = base64ToBlob(imageUrl)
|
|
469
|
+
formData.append('image', blob, 'image.png')
|
|
470
|
+
} else {
|
|
471
|
+
throw new Error('Invalid image URL format. Must be an HTTP URL or data URL.')
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
formData.append('n', String(count))
|
|
475
|
+
formData.append('size', size)
|
|
476
|
+
formData.append('response_format', format === 'b64_json' ? 'b64_json' : 'url')
|
|
477
|
+
if (model) {
|
|
478
|
+
formData.append('model', model)
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const response = await fetch(endpoint, {
|
|
482
|
+
method: 'POST',
|
|
483
|
+
headers: getAuthHeaders(),
|
|
484
|
+
body: formData,
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
if (!response.ok) {
|
|
488
|
+
const error = await response.text()
|
|
489
|
+
throw new Error(`Image variations failed: ${response.status} - ${error}`)
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const apiResponse = (await response.json()) as {
|
|
493
|
+
data?: Array<{
|
|
494
|
+
url?: string
|
|
495
|
+
b64_json?: string
|
|
496
|
+
}>
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (!apiResponse.data || apiResponse.data.length === 0) {
|
|
500
|
+
throw new Error('No variations returned from API')
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const duration = Date.now() - startTime
|
|
504
|
+
const dataLength = apiResponse.data.length
|
|
505
|
+
|
|
506
|
+
return apiResponse.data.map((item, index) => ({
|
|
507
|
+
url: item.url || (item.b64_json ? `data:image/png;base64,${item.b64_json}` : ''),
|
|
508
|
+
prompt: `Variation ${index + 1} of source image`,
|
|
509
|
+
metadata: {
|
|
510
|
+
model,
|
|
511
|
+
size,
|
|
512
|
+
duration: Math.round(duration / dataLength),
|
|
513
|
+
format,
|
|
514
|
+
},
|
|
515
|
+
}))
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Edit an image using a text prompt and optional mask
|
|
520
|
+
*
|
|
521
|
+
* @param imageUrl - URL or base64 of the source image
|
|
522
|
+
* @param options - Edit options including prompt and optional mask
|
|
523
|
+
* @returns Promise resolving to ImageResult
|
|
524
|
+
*
|
|
525
|
+
* @example
|
|
526
|
+
* ```ts
|
|
527
|
+
* // Simple edit
|
|
528
|
+
* const result = await image.edit('https://example.com/image.png', {
|
|
529
|
+
* prompt: 'Add a rainbow in the sky',
|
|
530
|
+
* })
|
|
531
|
+
* ```
|
|
532
|
+
*
|
|
533
|
+
* @example
|
|
534
|
+
* ```ts
|
|
535
|
+
* // Edit with mask
|
|
536
|
+
* const result = await image.edit('https://example.com/image.png', {
|
|
537
|
+
* prompt: 'A cat sitting on the couch',
|
|
538
|
+
* mask: 'https://example.com/mask.png', // Transparent areas will be edited
|
|
539
|
+
* })
|
|
540
|
+
* ```
|
|
541
|
+
*/
|
|
542
|
+
image.edit = async function edit(imageUrl: string, options: EditOptions): Promise<ImageResult> {
|
|
543
|
+
const startTime = Date.now()
|
|
544
|
+
|
|
545
|
+
const {
|
|
546
|
+
prompt,
|
|
547
|
+
mask,
|
|
548
|
+
size = DEFAULTS.size,
|
|
549
|
+
model = 'dall-e-2', // DALL-E 2 supports edits
|
|
550
|
+
n = 1,
|
|
551
|
+
format = DEFAULTS.format,
|
|
552
|
+
} = options
|
|
553
|
+
|
|
554
|
+
const baseUrl =
|
|
555
|
+
getEnv('AI_GATEWAY_URL') || getEnv('IMAGE_GATEWAY_URL') || 'https://image.workers.do'
|
|
556
|
+
const endpoint = `${baseUrl}/openai/images/edits`
|
|
557
|
+
|
|
558
|
+
const formData = new FormData()
|
|
559
|
+
|
|
560
|
+
// Add source image
|
|
561
|
+
if (imageUrl.startsWith('http')) {
|
|
562
|
+
const imageResponse = await fetch(imageUrl)
|
|
563
|
+
const imageBlob = await imageResponse.blob()
|
|
564
|
+
formData.append('image', imageBlob, 'image.png')
|
|
565
|
+
} else if (imageUrl.startsWith('data:')) {
|
|
566
|
+
const blob = base64ToBlob(imageUrl)
|
|
567
|
+
formData.append('image', blob, 'image.png')
|
|
568
|
+
} else {
|
|
569
|
+
throw new Error('Invalid image URL format. Must be an HTTP URL or data URL.')
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Add mask if provided
|
|
573
|
+
if (mask) {
|
|
574
|
+
if (mask.startsWith('http')) {
|
|
575
|
+
const maskResponse = await fetch(mask)
|
|
576
|
+
const maskBlob = await maskResponse.blob()
|
|
577
|
+
formData.append('mask', maskBlob, 'mask.png')
|
|
578
|
+
} else if (mask.startsWith('data:')) {
|
|
579
|
+
const maskBlob = base64ToBlob(mask)
|
|
580
|
+
formData.append('mask', maskBlob, 'mask.png')
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
formData.append('prompt', prompt)
|
|
585
|
+
formData.append('n', String(n))
|
|
586
|
+
formData.append('size', size)
|
|
587
|
+
formData.append('response_format', format === 'b64_json' ? 'b64_json' : 'url')
|
|
588
|
+
if (model) {
|
|
589
|
+
formData.append('model', model)
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const response = await fetch(endpoint, {
|
|
593
|
+
method: 'POST',
|
|
594
|
+
headers: getAuthHeaders(),
|
|
595
|
+
body: formData,
|
|
596
|
+
})
|
|
597
|
+
|
|
598
|
+
if (!response.ok) {
|
|
599
|
+
const error = await response.text()
|
|
600
|
+
throw new Error(`Image edit failed: ${response.status} - ${error}`)
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const apiResponse = (await response.json()) as {
|
|
604
|
+
data?: Array<{
|
|
605
|
+
url?: string
|
|
606
|
+
b64_json?: string
|
|
607
|
+
}>
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const imageData = apiResponse.data?.[0]
|
|
611
|
+
|
|
612
|
+
if (!imageData) {
|
|
613
|
+
throw new Error('No edited image returned from API')
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const editedUrl =
|
|
617
|
+
imageData.url || (imageData.b64_json ? `data:image/png;base64,${imageData.b64_json}` : '')
|
|
618
|
+
|
|
619
|
+
return {
|
|
620
|
+
url: editedUrl,
|
|
621
|
+
prompt,
|
|
622
|
+
metadata: {
|
|
623
|
+
model,
|
|
624
|
+
size,
|
|
625
|
+
duration: Date.now() - startTime,
|
|
626
|
+
format,
|
|
627
|
+
},
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Upscale an image to a higher resolution
|
|
633
|
+
*
|
|
634
|
+
* @param imageUrl - URL or base64 of the source image
|
|
635
|
+
* @param options - Upscale options
|
|
636
|
+
* @returns Promise resolving to UpscaleResult
|
|
637
|
+
*
|
|
638
|
+
* @example
|
|
639
|
+
* ```ts
|
|
640
|
+
* const result = await image.upscale('https://example.com/small.png', {
|
|
641
|
+
* scale: 4,
|
|
642
|
+
* })
|
|
643
|
+
*
|
|
644
|
+
* console.log(`Upscaled from ${result.originalSize.width}x${result.originalSize.height}`)
|
|
645
|
+
* console.log(`to ${result.upscaledSize.width}x${result.upscaledSize.height}`)
|
|
646
|
+
* ```
|
|
647
|
+
*/
|
|
648
|
+
image.upscale = async function upscale(
|
|
649
|
+
imageUrl: string,
|
|
650
|
+
options: UpscaleOptions = {}
|
|
651
|
+
): Promise<UpscaleResult> {
|
|
652
|
+
const startTime = Date.now()
|
|
653
|
+
|
|
654
|
+
const { scale = 2, model = 'real-esrgan', format = DEFAULTS.format, denoise } = options
|
|
655
|
+
|
|
656
|
+
const baseUrl =
|
|
657
|
+
getEnv('AI_GATEWAY_URL') || getEnv('IMAGE_GATEWAY_URL') || 'https://image.workers.do'
|
|
658
|
+
const endpoint = `${baseUrl}/upscale`
|
|
659
|
+
|
|
660
|
+
const payload: Record<string, unknown> = {
|
|
661
|
+
image: imageUrl,
|
|
662
|
+
scale,
|
|
663
|
+
model,
|
|
664
|
+
response_format: format,
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
if (denoise !== undefined) {
|
|
668
|
+
payload['denoise'] = denoise
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const response = await fetch(endpoint, {
|
|
672
|
+
method: 'POST',
|
|
673
|
+
headers: {
|
|
674
|
+
'Content-Type': 'application/json',
|
|
675
|
+
...getAuthHeaders(),
|
|
676
|
+
},
|
|
677
|
+
body: JSON.stringify(payload),
|
|
678
|
+
})
|
|
679
|
+
|
|
680
|
+
if (!response.ok) {
|
|
681
|
+
const error = await response.text()
|
|
682
|
+
throw new Error(`Image upscale failed: ${response.status} - ${error}`)
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const apiResponse = (await response.json()) as {
|
|
686
|
+
url?: string
|
|
687
|
+
b64_json?: string
|
|
688
|
+
original_width?: number
|
|
689
|
+
original_height?: number
|
|
690
|
+
upscaled_width?: number
|
|
691
|
+
upscaled_height?: number
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const upscaledUrl =
|
|
695
|
+
apiResponse.url || (apiResponse.b64_json ? `data:image/png;base64,${apiResponse.b64_json}` : '')
|
|
696
|
+
|
|
697
|
+
if (!upscaledUrl) {
|
|
698
|
+
throw new Error('No upscaled image returned from API')
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Extract dimensions from response or estimate
|
|
702
|
+
const originalWidth = apiResponse.original_width || 256
|
|
703
|
+
const originalHeight = apiResponse.original_height || 256
|
|
704
|
+
const upscaledWidth = apiResponse.upscaled_width || originalWidth * scale
|
|
705
|
+
const upscaledHeight = apiResponse.upscaled_height || originalHeight * scale
|
|
706
|
+
|
|
707
|
+
return {
|
|
708
|
+
url: upscaledUrl,
|
|
709
|
+
prompt: 'Upscaled image',
|
|
710
|
+
originalSize: {
|
|
711
|
+
width: originalWidth,
|
|
712
|
+
height: originalHeight,
|
|
713
|
+
},
|
|
714
|
+
upscaledSize: {
|
|
715
|
+
width: upscaledWidth,
|
|
716
|
+
height: upscaledHeight,
|
|
717
|
+
},
|
|
718
|
+
scaleFactor: scale,
|
|
719
|
+
metadata: {
|
|
720
|
+
model,
|
|
721
|
+
size: `${upscaledWidth}x${upscaledHeight}`,
|
|
722
|
+
duration: Date.now() - startTime,
|
|
723
|
+
format,
|
|
724
|
+
},
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Create a curried function for generating images with a specific style
|
|
730
|
+
*
|
|
731
|
+
* @param style - The style preset to use
|
|
732
|
+
* @returns A function that generates images with the specified style
|
|
733
|
+
*
|
|
734
|
+
* @example
|
|
735
|
+
* ```ts
|
|
736
|
+
* // Create a cinematic image generator
|
|
737
|
+
* const cinematicImage = image.style('cinematic')
|
|
738
|
+
*
|
|
739
|
+
* const result1 = await cinematicImage('A spaceship landing on Mars')
|
|
740
|
+
* const result2 = await cinematicImage('A detective in a rainy city')
|
|
741
|
+
* // Both images will have cinematic style applied
|
|
742
|
+
* ```
|
|
743
|
+
*
|
|
744
|
+
* @example
|
|
745
|
+
* ```ts
|
|
746
|
+
* // Create specialized generators
|
|
747
|
+
* const cartoonGen = image.style('cartoon')
|
|
748
|
+
* const realisticGen = image.style('realistic')
|
|
749
|
+
* const abstractGen = image.style('abstract')
|
|
750
|
+
*
|
|
751
|
+
* // Use them throughout your app
|
|
752
|
+
* const cartoonAvatar = await cartoonGen('A friendly robot')
|
|
753
|
+
* ```
|
|
754
|
+
*/
|
|
755
|
+
image.style = function style(stylePreset: ImageStyle) {
|
|
756
|
+
return function styledImage(
|
|
757
|
+
prompt: string,
|
|
758
|
+
options: Partial<Omit<ImageOptions, 'style'>> = {}
|
|
759
|
+
): Promise<ImageResult> {
|
|
760
|
+
return image(prompt, { ...options, style: stylePreset })
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* Batch generate multiple images from different prompts
|
|
766
|
+
*
|
|
767
|
+
* @param prompts - Array of prompts to generate
|
|
768
|
+
* @param options - Shared options for all generations
|
|
769
|
+
* @returns Promise resolving to array of ImageResults
|
|
770
|
+
*
|
|
771
|
+
* @example
|
|
772
|
+
* ```ts
|
|
773
|
+
* const results = await image.batch([
|
|
774
|
+
* 'A sunset over mountains',
|
|
775
|
+
* 'A forest in autumn',
|
|
776
|
+
* 'A city at night',
|
|
777
|
+
* ], { style: 'photographic' })
|
|
778
|
+
* ```
|
|
779
|
+
*/
|
|
780
|
+
image.batch = async function batch(
|
|
781
|
+
prompts: string[],
|
|
782
|
+
options: Partial<ImageOptions> = {}
|
|
783
|
+
): Promise<ImageResult[]> {
|
|
784
|
+
return Promise.all(prompts.map((prompt) => image(prompt, options)))
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* Generate an image with a specific aspect ratio
|
|
789
|
+
*
|
|
790
|
+
* @param prompt - The text prompt
|
|
791
|
+
* @param aspectRatio - The desired aspect ratio ('portrait' | 'landscape' | 'square')
|
|
792
|
+
* @param options - Additional options
|
|
793
|
+
* @returns Promise resolving to ImageResult
|
|
794
|
+
*
|
|
795
|
+
* @example
|
|
796
|
+
* ```ts
|
|
797
|
+
* // Generate a portrait image
|
|
798
|
+
* const portrait = await image.aspectRatio('A professional headshot', 'portrait')
|
|
799
|
+
*
|
|
800
|
+
* // Generate a landscape image
|
|
801
|
+
* const landscape = await image.aspectRatio('Mountain range panorama', 'landscape')
|
|
802
|
+
* ```
|
|
803
|
+
*/
|
|
804
|
+
image.aspectRatio = async function aspectRatio(
|
|
805
|
+
prompt: string,
|
|
806
|
+
ratio: 'portrait' | 'landscape' | 'square',
|
|
807
|
+
options: Partial<Omit<ImageOptions, 'size'>> = {}
|
|
808
|
+
): Promise<ImageResult> {
|
|
809
|
+
const sizeMap: Record<'portrait' | 'landscape' | 'square', ImageSize> = {
|
|
810
|
+
portrait: '1024x1792',
|
|
811
|
+
landscape: '1792x1024',
|
|
812
|
+
square: '1024x1024',
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
return image(prompt, { ...options, size: sizeMap[ratio] })
|
|
816
|
+
}
|