@fungi.computer/darkroom 0.0.0-stage → 0.1.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 (80) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +315 -2
  3. package/dist/app-media-contracts.d.ts +499 -0
  4. package/dist/app-media-contracts.js +96 -0
  5. package/dist/browser-failure.d.ts +40 -0
  6. package/dist/browser-failure.js +39 -0
  7. package/dist/browser.d.ts +2 -0
  8. package/dist/browser.js +1 -0
  9. package/dist/cloudflare/provider.d.ts +13 -0
  10. package/dist/cloudflare/provider.js +140 -0
  11. package/dist/cloudflare/reference-images.d.ts +42 -0
  12. package/dist/cloudflare/reference-images.js +102 -0
  13. package/dist/cloudflare.d.ts +6 -0
  14. package/dist/cloudflare.js +4 -0
  15. package/dist/cost-estimate.d.ts +22 -0
  16. package/dist/cost-estimate.js +76 -0
  17. package/dist/effect.d.ts +2 -0
  18. package/dist/effect.js +2 -0
  19. package/dist/execution-error.d.ts +54 -0
  20. package/dist/execution-error.js +62 -0
  21. package/dist/failure.d.ts +2 -0
  22. package/dist/failure.js +8 -0
  23. package/dist/gateway-models.d.ts +23 -0
  24. package/dist/gateway-models.js +77 -0
  25. package/dist/gateway-wire.d.ts +16 -0
  26. package/dist/gateway-wire.js +22 -0
  27. package/dist/index.d.ts +361 -0
  28. package/dist/index.js +607 -0
  29. package/dist/media-binding.d.ts +291 -0
  30. package/dist/media-binding.js +149 -0
  31. package/dist/media-generation.d.ts +10 -0
  32. package/dist/media-generation.js +110 -0
  33. package/dist/media-operation-contract.d.ts +104 -0
  34. package/dist/media-operation-contract.js +29 -0
  35. package/dist/media-operation-do.d.ts +55 -0
  36. package/dist/media-operation-do.js +105 -0
  37. package/dist/media-operation-owner.d.ts +301 -0
  38. package/dist/media-operation-owner.js +903 -0
  39. package/dist/media-operation-storage.d.ts +81 -0
  40. package/dist/media-operation-storage.js +1 -0
  41. package/dist/media-operation.d.ts +33 -0
  42. package/dist/media-operation.js +230 -0
  43. package/dist/model-catalog.d.ts +456 -0
  44. package/dist/model-catalog.js +565 -0
  45. package/dist/neurons.d.ts +4 -0
  46. package/dist/neurons.js +4 -0
  47. package/dist/provenance-bytes.d.ts +3 -0
  48. package/dist/provenance-bytes.js +15 -0
  49. package/dist/provenance-export.d.ts +23 -0
  50. package/dist/provenance-export.js +103 -0
  51. package/dist/provenance-jpeg.d.ts +3 -0
  52. package/dist/provenance-jpeg.js +76 -0
  53. package/dist/provenance-png.d.ts +1 -0
  54. package/dist/provenance-png.js +21 -0
  55. package/dist/provenance-xmp.d.ts +2 -0
  56. package/dist/provenance-xmp.js +54 -0
  57. package/dist/provenance.d.ts +20 -0
  58. package/dist/provenance.js +58 -0
  59. package/dist/server.d.ts +158 -0
  60. package/dist/server.js +207 -0
  61. package/dist/video-providers/index.d.ts +14 -0
  62. package/dist/video-providers/index.js +14 -0
  63. package/dist/video-providers/minimax.d.ts +25 -0
  64. package/dist/video-providers/minimax.js +63 -0
  65. package/dist/video-providers/reference-encoding.d.ts +4 -0
  66. package/dist/video-providers/reference-encoding.js +11 -0
  67. package/dist/video-providers/types.d.ts +28 -0
  68. package/dist/video-providers/types.js +1 -0
  69. package/dist/video-providers/unified.d.ts +18 -0
  70. package/dist/video-providers/unified.js +52 -0
  71. package/docs/api/README.md +16 -0
  72. package/docs/api/app-media-contracts.md +507 -0
  73. package/docs/api/browser.md +126 -0
  74. package/docs/api/cloudflare.md +678 -0
  75. package/docs/api/effect.md +39 -0
  76. package/docs/api/index.md +1775 -0
  77. package/docs/api/modules.md +19 -0
  78. package/docs/api/provenance-export.md +114 -0
  79. package/docs/api/server.md +1672 -0
  80. package/package.json +110 -3
package/dist/index.js ADDED
@@ -0,0 +1,607 @@
1
+ import * as z from "zod";
2
+ import { reportCatchFailure } from "./failure.js";
3
+ import { getMediaModel, MediaSettingsError, mediaVideoModelIdSchema, mediaVideoResolutionSchema, mediaVideoAspectRatioSchema, mediaVideoReferenceRoleSchema, } from "./model-catalog.js";
4
+ export { mediaFailureStageSchema } from "./execution-error.js";
5
+ export { getMediaModel, listMediaModels, mediaModelInputSchema, mediaModelSchema, mediaRequestedImageFormatSchema, mediaModels, mediaModelTierSchema, MediaSettingsError, mediaVideoModelIdSchema, mediaVideoResolutionSchema, mediaVideoAspectRatioSchema, mediaVideoReferenceRoleSchema, mediaVideoFormatSchema, mediaVideoSchema, } from "./model-catalog.js";
6
+ /** The long-edge bounds a viewer may ask for when fitting an image to look at. */
7
+ export const MIN_VIEW_DIMENSION = 256;
8
+ /** Maximum long-edge size accepted by an image viewer, in pixels. */
9
+ export const MAX_VIEW_DIMENSION = 512;
10
+ /** Decode a viewer long-edge size within the supported pixel bounds. */
11
+ export const viewDimensionSchema = z.lazy(() => z.number().int().min(MIN_VIEW_DIMENSION).max(MAX_VIEW_DIMENSION));
12
+ /** Maximum reference files in one generation request. */
13
+ export const MAX_REFERENCE_COUNT = 4;
14
+ /** Maximum reference width and height sent to inference, in pixels. */
15
+ export const MAX_REFERENCE_DIMENSION = 512;
16
+ /** Maximum decoded bytes per reference image. */
17
+ export const MAX_REFERENCE_BYTES = 8 * 1024 * 1024;
18
+ /** Maximum retained still-image output bytes. */
19
+ export const MAX_OUTPUT_BYTES = 32 * 1024 * 1024;
20
+ const requiredString = (value, name) => {
21
+ const trimmed = value.trim();
22
+ if (trimmed.length === 0)
23
+ throw new MediaSettingsError(`${name} must not be blank`);
24
+ return trimmed;
25
+ };
26
+ const boundedInteger = (value, name, minimum, maximum) => {
27
+ if (!Number.isSafeInteger(value) || value < minimum || value > maximum)
28
+ throw new MediaSettingsError(`${name} must be an integer from ${String(minimum)} through ${String(maximum)}`);
29
+ return value;
30
+ };
31
+ const normalizeReference = (value) => {
32
+ const reference = {
33
+ computerId: requiredString(value.computerId, "reference computerId"),
34
+ path: requiredString(value.path, "reference path"),
35
+ };
36
+ if (value.role === undefined)
37
+ return reference;
38
+ return { ...reference, role: value.role };
39
+ };
40
+ /** Structural wire grammar for one Media generation request. */
41
+ export const mediaGenerationSettingsSchema = z
42
+ .object({
43
+ /** Provider catalog model identifier. */
44
+ model: z.string().min(1).max(256),
45
+ /** Generation prompt. */
46
+ prompt: z.string().min(1).max(2_048),
47
+ /** Requested image width or nominal video selector, not decoded MP4 dimensions. */
48
+ width: z.number().int().min(1).max(5_040),
49
+ /** Requested image height or nominal video selector, not decoded MP4 dimensions. */
50
+ height: z.number().int().min(1).max(5_040),
51
+ /** Model-specific inference step count. */
52
+ steps: z.number().int().optional(),
53
+ /** Requested video length in whole seconds. */
54
+ durationSeconds: z.number().int().optional(),
55
+ /** Video resolution; must match the nominal width/height selector. */
56
+ resolution: mediaVideoResolutionSchema.optional(),
57
+ /** Video aspect; must match the nominal width/height selector. */
58
+ aspectRatio: mediaVideoAspectRatioSchema.optional(),
59
+ /** Video audio selection. */
60
+ generateAudio: z.boolean().optional(),
61
+ /** Host-authorized reference images. */
62
+ references: z
63
+ .array(z
64
+ .object({
65
+ /** Host-admitted destination or reference identity. */
66
+ computerId: z.string().min(1).max(256),
67
+ /** Fixed destination or reference file path. */
68
+ path: z.string().min(1).max(4_096),
69
+ /** Video-only reference role. */
70
+ role: mediaVideoReferenceRoleSchema.optional(),
71
+ })
72
+ .strict())
73
+ .max(MAX_REFERENCE_COUNT)
74
+ .optional(),
75
+ })
76
+ .strict();
77
+ /** App selections contain paths; the host supplies the admitted Computer. */
78
+ export const mediaGenerationSelectionSchema = mediaGenerationSettingsSchema.extend({
79
+ /** Host-authorized reference images. */
80
+ references: z
81
+ .array(z
82
+ .object({
83
+ /** Fixed destination or reference file path. */
84
+ path: z.string().min(1).max(4_096),
85
+ /** Video-only reference role. */
86
+ role: mediaVideoReferenceRoleSchema.optional(),
87
+ })
88
+ .strict())
89
+ .max(MAX_REFERENCE_COUNT)
90
+ .optional(),
91
+ });
92
+ /** Validate caller settings and apply retained model defaults. */
93
+ export const validateMediaGenerationSettings = (raw) => {
94
+ const parsed = mediaGenerationSettingsSchema.safeParse(raw);
95
+ if (!parsed.success)
96
+ throw new MediaSettingsError("generation settings are invalid");
97
+ return normalizeGenerationSettings(parsed.data, normalizeReference);
98
+ };
99
+ /** Validate path-only selections without inventing a Computer identity. */
100
+ export const validateMediaGenerationSelection = (raw) => {
101
+ const parsed = mediaGenerationSelectionSchema.safeParse(raw);
102
+ if (!parsed.success)
103
+ throw new MediaSettingsError("generation settings are invalid");
104
+ return normalizeGenerationSettings(parsed.data, (reference) => {
105
+ const selection = {
106
+ path: requiredString(reference.path, "reference path"),
107
+ };
108
+ if (reference.role === undefined)
109
+ return selection;
110
+ return { ...selection, role: reference.role };
111
+ });
112
+ };
113
+ function normalizeGenerationSettings(input, normalize) {
114
+ const modelId = requiredString(input.model, "model");
115
+ const model = getMediaModel(modelId);
116
+ if (model === undefined)
117
+ throw new MediaSettingsError(`unknown Media model "${modelId}"`);
118
+ const prompt = requiredString(input.prompt, "prompt");
119
+ const maximumDimension = model.video === null ? 4_096 : 5_040;
120
+ const width = boundedInteger(input.width, "width", 1, maximumDimension);
121
+ const height = boundedInteger(input.height, "height", 1, maximumDimension);
122
+ if (model.input.dimensions === "own" &&
123
+ (width < 256 ||
124
+ height < 256 ||
125
+ width > (model.input.maxDimension ?? 4_096) ||
126
+ height > (model.input.maxDimension ?? 4_096)))
127
+ throw new MediaSettingsError(`${model.id} accepts dimensions from 256 through ${String(model.input.maxDimension)}`);
128
+ if (model.sizes !== null &&
129
+ !model.sizes.includes(`${String(width)}x${String(height)}`))
130
+ throw new MediaSettingsError(`${model.id} accepts only these sizes: ${model.sizes.join(", ")}`);
131
+ let durationSeconds;
132
+ if (model.video === null) {
133
+ if (input.resolution !== undefined ||
134
+ input.aspectRatio !== undefined ||
135
+ input.generateAudio !== undefined ||
136
+ input.references?.some((reference) => reference.role !== undefined))
137
+ throw new MediaSettingsError(`${model.id} does not take video settings or frame roles`);
138
+ if (input.durationSeconds !== undefined)
139
+ throw new MediaSettingsError(`${model.id} does not take a duration`);
140
+ }
141
+ else {
142
+ durationSeconds = boundedInteger(input.durationSeconds ?? model.video.defaultSeconds, "durationSeconds", model.video.minSeconds, model.video.maxSeconds);
143
+ if (!model.video.durations.includes(durationSeconds))
144
+ throw new MediaSettingsError(`${model.id} accepts durationSeconds ${model.video.durations.join(", ")}`);
145
+ if (prompt.length > model.video.maxPromptLength)
146
+ throw new MediaSettingsError(`${model.id} prompt exceeds ${String(model.video.maxPromptLength)} characters`);
147
+ }
148
+ let steps;
149
+ if (input.steps !== undefined && model.input.steps === "fixed")
150
+ throw new MediaSettingsError(`${model.id} uses fixed steps and cannot be adjusted`);
151
+ if (input.steps !== undefined) {
152
+ steps = boundedInteger(input.steps, "steps", 1, Number.MAX_SAFE_INTEGER);
153
+ if (model.input.maxSteps !== null && steps > model.input.maxSteps)
154
+ throw new MediaSettingsError(`${model.id} accepts at most ${String(model.input.maxSteps)} steps`);
155
+ }
156
+ else if (model.input.steps !== "fixed" &&
157
+ model.input.defaultSteps !== null) {
158
+ steps = model.input.defaultSteps;
159
+ }
160
+ let references;
161
+ if (input.references !== undefined) {
162
+ if (input.references.length > model.input.maxReferences)
163
+ throw new MediaSettingsError(`${model.id} accepts at most ${String(model.input.maxReferences)} reference images`);
164
+ references = input.references.map(normalize);
165
+ }
166
+ const normalized = {
167
+ model: model.id,
168
+ prompt,
169
+ width,
170
+ height,
171
+ };
172
+ if (steps !== undefined)
173
+ normalized.steps = steps;
174
+ if (durationSeconds !== undefined)
175
+ normalized.durationSeconds = durationSeconds;
176
+ if (model.video !== null) {
177
+ const video = model.video;
178
+ const format = video.formats.find((candidate) => candidate.width === width && candidate.height === height);
179
+ if (format === undefined)
180
+ throw new MediaSettingsError("unsupported video size");
181
+ if ((input.resolution !== undefined &&
182
+ input.resolution !== format.resolution) ||
183
+ (input.aspectRatio !== undefined &&
184
+ input.aspectRatio !== format.aspectRatio))
185
+ throw new MediaSettingsError("video selectors must match nominal width and height");
186
+ normalized.resolution = format.resolution;
187
+ normalized.aspectRatio = format.aspectRatio;
188
+ const generateAudio = input.generateAudio ?? video.defaultGenerateAudio;
189
+ if (generateAudio !== null)
190
+ normalized.generateAudio = generateAudio;
191
+ if (references !== undefined) {
192
+ references = references.map((reference) => ({
193
+ ...reference,
194
+ role: reference.role ?? mediaVideoReferenceRoleSchema.enum.first,
195
+ }));
196
+ const roles = references.map((reference) => reference.role);
197
+ if (roles.some((role) => role === undefined || !video.referenceRoles.includes(role)))
198
+ throw new MediaSettingsError(`${model.id} does not accept this frame/reference role`);
199
+ const first = roles.filter((role) => role === "first-frame").length;
200
+ const last = roles.filter((role) => role === "last-frame").length;
201
+ const referenceCount = roles.filter((role) => role === "reference").length;
202
+ if (first > 1 ||
203
+ last > 1 ||
204
+ (last > 0 && first !== 1) ||
205
+ (referenceCount > 0 && first + last > 0))
206
+ throw new MediaSettingsError("video frame and reference modes cannot mix; last frame requires one first frame");
207
+ if (referenceCount > video.maxReferenceImages ||
208
+ (referenceCount > 0 &&
209
+ !video.referenceResolutions.includes(format.resolution)))
210
+ throw new MediaSettingsError(`${model.id} does not accept reference-image mode at this resolution/count`);
211
+ }
212
+ }
213
+ if (references !== undefined)
214
+ normalized.references = Object.freeze(references);
215
+ return Object.freeze(normalized);
216
+ }
217
+ /** Path-only video reference; the host supplies authority and reads its bytes. */
218
+ export const mediaVideoReferenceSchema = z.lazy(() => z.strictObject({
219
+ /** Selected image path, trimmed and nonblank after validation. */
220
+ path: z.string().min(1).max(4_096),
221
+ /** Explicit first frame, last frame, or reference-image role. */
222
+ role: mediaVideoReferenceRoleSchema,
223
+ }));
224
+ /** Neutral video selection; model-specific combinations are checked by validateMediaVideoSelection. */
225
+ export const mediaVideoSelectionSchema = z.lazy(() => z.strictObject({
226
+ /** Verified catalog model identifier. */
227
+ model: mediaVideoModelIdSchema,
228
+ /** Trimmed prompt; Seedance allows 2,000 characters, Veo uses the retained host cap of 2,048. */
229
+ prompt: z.string().min(1).max(2_048),
230
+ /** Model-specific resolution, default 720p. */
231
+ resolution: mediaVideoResolutionSchema.optional(),
232
+ /** Model-specific aspect, default 16:9. */
233
+ aspectRatio: mediaVideoAspectRatioSchema.optional(),
234
+ /** Whole seconds; Seedance 4–12, Veo exactly 4/6/8, MiniMax H3 Max 5–15. */
235
+ durationSeconds: z.number().int().min(4).max(15).optional(),
236
+ /** Veo defaults true; Seedance omits the provider option when unset;
237
+ * MiniMax H3 Max has no audio field and refuses an explicit choice. */
238
+ generateAudio: z.boolean().optional(),
239
+ /** Up to four references, with provider-specific roles and mode limits. */
240
+ references: z
241
+ .array(mediaVideoReferenceSchema)
242
+ .max(MAX_REFERENCE_COUNT)
243
+ .optional(),
244
+ }));
245
+ /** Validate supported model/size/duration/reference combinations before authority or billing admission. */
246
+ export function validateMediaVideoSelection(raw) {
247
+ const parsed = mediaVideoSelectionSchema.safeParse(raw);
248
+ if (!parsed.success)
249
+ throw new MediaSettingsError("video selection is invalid");
250
+ const input = parsed.data;
251
+ const model = getMediaModel(input.model);
252
+ if (model?.video == null)
253
+ throw new MediaSettingsError("unknown video model");
254
+ const resolution = input.resolution ?? model.video.defaultResolution;
255
+ const aspectRatio = input.aspectRatio ?? model.video.defaultAspectRatio;
256
+ const format = model.video.formats.find((candidate) => candidate.resolution === resolution &&
257
+ candidate.aspectRatio === aspectRatio);
258
+ if (format === undefined)
259
+ throw new MediaSettingsError(`${model.id} does not accept this resolution/aspect`);
260
+ const normalized = validateMediaGenerationSelection({
261
+ ...input,
262
+ width: format.width,
263
+ height: format.height,
264
+ });
265
+ const selection = {
266
+ ...format,
267
+ model: input.model,
268
+ prompt: normalized.prompt,
269
+ durationSeconds: normalized.durationSeconds ?? model.video.defaultSeconds,
270
+ };
271
+ if (normalized.generateAudio !== undefined)
272
+ selection.generateAudio = normalized.generateAudio;
273
+ if (normalized.references !== undefined)
274
+ selection.references = Object.freeze(normalized.references.map((reference) => Object.freeze(mediaVideoReferenceSchema.parse(reference))));
275
+ return Object.freeze(selection);
276
+ }
277
+ const imageFormats = new Set(["png", "jpeg", "gif", "webp"]);
278
+ /** Resolve only provider-accepted references through the caller's reader. */
279
+ export const resolveMediaReferences = async (settings, reader) => {
280
+ const model = getMediaModel(settings.model);
281
+ if (model === undefined)
282
+ throw new MediaSettingsError("unknown Media model");
283
+ const references = settings.references ?? [];
284
+ if (references.length > model.input.maxReferences)
285
+ throw new MediaSettingsError(`${model.id} accepts at most ${String(model.input.maxReferences)} reference images`);
286
+ const resolved = [];
287
+ for (const reference of references) {
288
+ const image = await reader.read(reference);
289
+ if (!imageFormats.has(image.format) ||
290
+ !Number.isSafeInteger(image.width) ||
291
+ !Number.isSafeInteger(image.height) ||
292
+ image.width < 1 ||
293
+ image.height < 1 ||
294
+ image.width > MAX_REFERENCE_DIMENSION ||
295
+ image.height > MAX_REFERENCE_DIMENSION ||
296
+ !(image.bytes instanceof Uint8Array) ||
297
+ image.bytes.byteLength === 0 ||
298
+ image.bytes.byteLength > MAX_REFERENCE_BYTES)
299
+ throw new MediaSettingsError(`reference ${reference.computerId}:${reference.path} exceeds the bounded image contract`);
300
+ resolved.push(Object.freeze({ ...image }));
301
+ }
302
+ return Object.freeze(resolved);
303
+ };
304
+ const mimeForFormat = (format) => `image/${format}`;
305
+ /** Build the exact JSON or multipart payload required by a retained model. */
306
+ export const buildProviderInput = (settings, references = []) => {
307
+ const model = getMediaModel(settings.model);
308
+ if (model === undefined)
309
+ throw new MediaSettingsError("unknown Media model");
310
+ if (model.sizes !== null)
311
+ throw new MediaSettingsError(`${model.id} runs through the AI Gateway`);
312
+ if (references.length > model.input.maxReferences)
313
+ throw new MediaSettingsError(`${model.id} accepts at most ${String(model.input.maxReferences)} reference images`);
314
+ if (model.input.referenceField === null && references.length > 0)
315
+ throw new MediaSettingsError(`${model.id} does not accept references`);
316
+ if (model.input.transport === "json") {
317
+ const body = {
318
+ prompt: settings.prompt,
319
+ };
320
+ if (model.input.dimensions === "own") {
321
+ body.width = settings.width;
322
+ body.height = settings.height;
323
+ }
324
+ if (settings.steps !== undefined && model.input.steps === "steps")
325
+ body.steps = settings.steps;
326
+ if (settings.steps !== undefined && model.input.steps === "num_steps")
327
+ body.num_steps = settings.steps;
328
+ return Object.freeze({
329
+ transport: "json",
330
+ body: Object.freeze(body),
331
+ });
332
+ }
333
+ const body = new FormData();
334
+ body.append("prompt", settings.prompt);
335
+ if (model.input.dimensions === "own") {
336
+ body.append("width", String(settings.width));
337
+ body.append("height", String(settings.height));
338
+ }
339
+ if (settings.steps !== undefined && model.input.steps === "steps")
340
+ body.append("steps", String(settings.steps));
341
+ references.forEach((reference, index) => {
342
+ body.append(`${model.input.referenceField ?? "input_image_"}${String(index)}`, new Blob([new Uint8Array(reference.bytes).buffer], {
343
+ type: mimeForFormat(reference.format),
344
+ }), reference.filename ?? `reference-${String(index)}`);
345
+ });
346
+ return Object.freeze({
347
+ transport: "multipart",
348
+ body,
349
+ });
350
+ };
351
+ const providerEntrySchema = z.lazy(() => z
352
+ .object({
353
+ /** Base64-encoded image data. */
354
+ base64: z.string().optional(),
355
+ /** Encoded inline image data. */
356
+ data: z.string().optional(),
357
+ /** HTTP provider output URL. */
358
+ url: z
359
+ .url()
360
+ .refine((value) => /^https?:\/\//iu.test(value), "provider image URLs must use HTTP(S)")
361
+ .optional(),
362
+ })
363
+ .strict());
364
+ const providerEnvelopeSchema = z.lazy(() => z.union([
365
+ z.object({
366
+ /** Encoded provider image. */
367
+ image: z.string().min(1),
368
+ }),
369
+ z.object({
370
+ /** Provider image entries. */
371
+ image: z.array(providerEntrySchema),
372
+ }),
373
+ z.object({
374
+ /** Provider output image entries. */
375
+ images: z.array(providerEntrySchema),
376
+ }),
377
+ ]));
378
+ /** Decode the Workers AI binding's raw output at the provider boundary. */
379
+ export const mediaProviderWireSchema = z.lazy(() => z.union([
380
+ providerEnvelopeSchema,
381
+ z.instanceof(Uint8Array),
382
+ z.instanceof(Response),
383
+ z.custom((value) => value instanceof ReadableStream),
384
+ ]));
385
+ /** Parse the provider wire envelope before normalization. */
386
+ export const parseMediaProviderOutput = (value) => {
387
+ if (value instanceof Uint8Array)
388
+ return {
389
+ kind: "bytes",
390
+ bytes: value,
391
+ };
392
+ if (value instanceof Response)
393
+ return {
394
+ kind: "response",
395
+ response: value,
396
+ };
397
+ if (value instanceof ReadableStream)
398
+ return {
399
+ kind: "response",
400
+ response: new Response(value),
401
+ };
402
+ const parsed = providerEnvelopeSchema.parse(value);
403
+ if ("image" in parsed)
404
+ return Array.isArray(parsed.image)
405
+ ? {
406
+ kind: "entries",
407
+ entries: parsed.image,
408
+ }
409
+ : {
410
+ kind: "encoded",
411
+ encoded: parsed.image,
412
+ };
413
+ return {
414
+ kind: "entries",
415
+ entries: parsed.images,
416
+ };
417
+ };
418
+ const sniffImageFormat = (bytes) => {
419
+ if (bytes.length >= 8 &&
420
+ bytes[0] === 0x89 &&
421
+ bytes[1] === 0x50 &&
422
+ bytes[2] === 0x4e &&
423
+ bytes[3] === 0x47 &&
424
+ bytes[4] === 0x0d &&
425
+ bytes[5] === 0x0a &&
426
+ bytes[6] === 0x1a &&
427
+ bytes[7] === 0x0a)
428
+ return "png";
429
+ if (bytes.length >= 3 &&
430
+ bytes[0] === 0xff &&
431
+ bytes[1] === 0xd8 &&
432
+ bytes[2] === 0xff)
433
+ return "jpeg";
434
+ if (bytes.length >= 6 &&
435
+ bytes[0] === 0x47 &&
436
+ bytes[1] === 0x49 &&
437
+ bytes[2] === 0x46 &&
438
+ bytes[3] === 0x38 &&
439
+ (bytes[4] === 0x37 || bytes[4] === 0x39) &&
440
+ bytes[5] === 0x61)
441
+ return "gif";
442
+ if (bytes.length >= 12 &&
443
+ bytes[0] === 0x52 &&
444
+ bytes[1] === 0x49 &&
445
+ bytes[2] === 0x46 &&
446
+ bytes[3] === 0x46 &&
447
+ bytes[8] === 0x57 &&
448
+ bytes[9] === 0x45 &&
449
+ bytes[10] === 0x42 &&
450
+ bytes[11] === 0x50)
451
+ return "webp";
452
+ return undefined;
453
+ };
454
+ const decodeEncoded = (value, maximumBytes) => {
455
+ const comma = value.startsWith("data:") ? value.indexOf(",") : -1;
456
+ const payload = comma >= 0 ? value.slice(comma + 1) : value;
457
+ if (payload.length > Math.ceil((maximumBytes * 4) / 3) + 4)
458
+ return undefined;
459
+ const encoded = z.base64().safeParse(payload);
460
+ if (!encoded.success)
461
+ return undefined;
462
+ const binary = atob(encoded.data);
463
+ if (binary.length === 0 || binary.length > maximumBytes)
464
+ return undefined;
465
+ return Uint8Array.from(binary, (character) => character.charCodeAt(0));
466
+ };
467
+ const encodedExceedsBound = (value, maximumBytes) => {
468
+ const comma = value.startsWith("data:") ? value.indexOf(",") : -1;
469
+ const payload = comma >= 0 ? value.slice(comma + 1) : value;
470
+ return payload.length > Math.ceil((maximumBytes * 4) / 3) + 4;
471
+ };
472
+ const declaredDataUrlFormat = (value) => {
473
+ if (!value.startsWith("data:"))
474
+ return undefined;
475
+ const comma = value.indexOf(",");
476
+ if (comma < 0)
477
+ return undefined;
478
+ const mime = value.slice(5, comma).split(";", 1)[0]?.toLowerCase();
479
+ if (mime === "image/jpeg")
480
+ return "jpeg";
481
+ if (mime === "image/png")
482
+ return "png";
483
+ if (mime === "image/gif")
484
+ return "gif";
485
+ if (mime === "image/webp")
486
+ return "webp";
487
+ return undefined;
488
+ };
489
+ /** Read a successful provider response within an explicit byte bound, closing its reader. */
490
+ export const readResponseBytes = async (response, maximumBytes) => {
491
+ if (!response.ok)
492
+ throw new Error(`provider response status ${String(response.status)}`);
493
+ const claimedLength = response.headers.get("content-length");
494
+ if (claimedLength !== null) {
495
+ const length = Number(claimedLength);
496
+ if (!Number.isSafeInteger(length) || length < 0 || length > maximumBytes)
497
+ throw new Error("provider response exceeds the byte bound");
498
+ }
499
+ if (response.body === null)
500
+ throw new Error("provider response had no body");
501
+ const reader = new ReadableStreamDefaultReader(response.body);
502
+ const chunks = [];
503
+ let total = 0;
504
+ try {
505
+ let item = await reader.read();
506
+ while (!item.done) {
507
+ total += item.value.byteLength;
508
+ if (total > maximumBytes)
509
+ throw new Error("provider response exceeds the byte bound");
510
+ chunks.push(item.value);
511
+ item = await reader.read();
512
+ }
513
+ }
514
+ finally {
515
+ // Closing a rejected stream must not hide its original read/bound failure.
516
+ await reader.cancel().catch((cause) => {
517
+ reportCatchFailure("provider-response-reader-cancel", cause);
518
+ });
519
+ reader.releaseLock();
520
+ }
521
+ const bytes = new Uint8Array(total);
522
+ let offset = 0;
523
+ for (const chunk of chunks) {
524
+ bytes.set(chunk, offset);
525
+ offset += chunk.byteLength;
526
+ }
527
+ return bytes;
528
+ };
529
+ const tryNormalizeBytes = (bytes, maximumBytes, declaredFormat) => {
530
+ if (bytes.byteLength === 0 || bytes.byteLength > maximumBytes)
531
+ return undefined;
532
+ const format = sniffImageFormat(bytes);
533
+ if (format === undefined)
534
+ return undefined;
535
+ return { bytes, format: declaredFormat ?? format };
536
+ };
537
+ const normalizeBytes = (bytes, maximumBytes, declaredFormat) => {
538
+ const output = tryNormalizeBytes(bytes, maximumBytes, declaredFormat);
539
+ if (output === undefined)
540
+ throw new Error("provider image has an unsupported format or size");
541
+ return output;
542
+ };
543
+ /** Normalize all retained provider envelopes into bounded image bytes. */
544
+ export const normalizeMediaProviderOutput = async (value, options = {}) => {
545
+ const maximumBytes = options.maxBytes ?? MAX_OUTPUT_BYTES;
546
+ if (!Number.isSafeInteger(maximumBytes) || maximumBytes < 1)
547
+ throw new Error("maxBytes must be a positive safe integer");
548
+ const parsed = parseMediaProviderOutput(value);
549
+ if (parsed.kind === "bytes")
550
+ return normalizeBytes(parsed.bytes, maximumBytes);
551
+ if (parsed.kind === "response")
552
+ return normalizeBytes(await readResponseBytes(parsed.response, maximumBytes), maximumBytes);
553
+ if (parsed.kind === "encoded") {
554
+ if (encodedExceedsBound(parsed.encoded, maximumBytes))
555
+ throw new Error("provider image exceeds the byte bound");
556
+ const bytes = decodeEncoded(parsed.encoded, maximumBytes);
557
+ if (bytes === undefined)
558
+ throw new Error("provider image encoding is invalid");
559
+ return normalizeBytes(bytes, maximumBytes, declaredDataUrlFormat(parsed.encoded));
560
+ }
561
+ for (const entry of parsed.entries) {
562
+ for (const encoded of [entry.base64, entry.data]) {
563
+ if (encoded === undefined)
564
+ continue;
565
+ const bytes = decodeEncoded(encoded, maximumBytes);
566
+ if (bytes !== undefined) {
567
+ const normalized = tryNormalizeBytes(bytes, maximumBytes, declaredDataUrlFormat(encoded));
568
+ if (normalized !== undefined)
569
+ return normalized;
570
+ }
571
+ }
572
+ if (entry.url !== undefined) {
573
+ const fetchUrl = options.fetchUrl ?? ((url) => fetch(url));
574
+ try {
575
+ const response = await fetchUrl(entry.url);
576
+ return normalizeBytes(await readResponseBytes(response, maximumBytes), maximumBytes);
577
+ }
578
+ catch (cause) {
579
+ reportCatchFailure("provider-entry-fetch", cause);
580
+ /* Try the next provider entry. */
581
+ }
582
+ }
583
+ }
584
+ throw new Error("provider returned no valid image bytes");
585
+ };
586
+ export { estimateImageMilliunits, estimateImageNeurons, } from "./cost-estimate.js";
587
+ export { neuronsToMilliunits, USD_PER_1K_NEURONS } from "./neurons.js";
588
+ /** Provider video cost from the sole catalog, in milliunits (1 cent = 1,000). Video-input references are unsupported. */
589
+ export function estimateVideoMilliunits(settings) {
590
+ const checked = validateMediaGenerationSelection({
591
+ ...settings,
592
+ references: settings.references?.map((reference) => ({
593
+ path: reference.path,
594
+ role: reference.role,
595
+ })),
596
+ });
597
+ const model = getMediaModel(checked.model);
598
+ if (model?.video == null || checked.durationSeconds === undefined)
599
+ throw new MediaSettingsError("video cost requires a verified video model");
600
+ const price = model.video.prices.find((candidate) => candidate.resolution === checked.resolution);
601
+ if (price === undefined)
602
+ throw new MediaSettingsError("video price unavailable for this resolution");
603
+ const rate = checked.generateAudio === true
604
+ ? price.audioMilliunitsPerSecond
605
+ : price.silentMilliunitsPerSecond;
606
+ return rate * checked.durationSeconds;
607
+ }