@fungi.computer/darkroom 0.0.0-stage → 0.0.1
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/LICENSE +21 -0
- package/README.md +315 -2
- package/dist/app-media-contracts.d.ts +499 -0
- package/dist/app-media-contracts.js +98 -0
- package/dist/browser-failure.d.ts +40 -0
- package/dist/browser-failure.js +39 -0
- package/dist/browser.d.ts +2 -0
- package/dist/browser.js +1 -0
- package/dist/cloudflare/provider.d.ts +13 -0
- package/dist/cloudflare/provider.js +140 -0
- package/dist/cloudflare/reference-images.d.ts +42 -0
- package/dist/cloudflare/reference-images.js +102 -0
- package/dist/cloudflare.d.ts +6 -0
- package/dist/cloudflare.js +4 -0
- package/dist/cost-estimate.d.ts +22 -0
- package/dist/cost-estimate.js +76 -0
- package/dist/effect.d.ts +2 -0
- package/dist/effect.js +2 -0
- package/dist/execution-error.d.ts +54 -0
- package/dist/execution-error.js +62 -0
- package/dist/failure.d.ts +2 -0
- package/dist/failure.js +8 -0
- package/dist/gateway-models.d.ts +23 -0
- package/dist/gateway-models.js +77 -0
- package/dist/gateway-wire.d.ts +16 -0
- package/dist/gateway-wire.js +22 -0
- package/dist/index.d.ts +361 -0
- package/dist/index.js +611 -0
- package/dist/media-binding.d.ts +291 -0
- package/dist/media-binding.js +146 -0
- package/dist/media-generation.d.ts +10 -0
- package/dist/media-generation.js +110 -0
- package/dist/media-operation-contract.d.ts +104 -0
- package/dist/media-operation-contract.js +29 -0
- package/dist/media-operation-do.d.ts +55 -0
- package/dist/media-operation-do.js +105 -0
- package/dist/media-operation-owner.d.ts +301 -0
- package/dist/media-operation-owner.js +903 -0
- package/dist/media-operation-storage.d.ts +81 -0
- package/dist/media-operation-storage.js +1 -0
- package/dist/media-operation.d.ts +33 -0
- package/dist/media-operation.js +230 -0
- package/dist/model-catalog.d.ts +456 -0
- package/dist/model-catalog.js +565 -0
- package/dist/neurons.d.ts +4 -0
- package/dist/neurons.js +4 -0
- package/dist/provenance-bytes.d.ts +3 -0
- package/dist/provenance-bytes.js +15 -0
- package/dist/provenance-export.d.ts +23 -0
- package/dist/provenance-export.js +103 -0
- package/dist/provenance-jpeg.d.ts +3 -0
- package/dist/provenance-jpeg.js +76 -0
- package/dist/provenance-png.d.ts +1 -0
- package/dist/provenance-png.js +21 -0
- package/dist/provenance-xmp.d.ts +2 -0
- package/dist/provenance-xmp.js +54 -0
- package/dist/provenance.d.ts +20 -0
- package/dist/provenance.js +58 -0
- package/dist/server.d.ts +158 -0
- package/dist/server.js +207 -0
- package/dist/video-providers/index.d.ts +14 -0
- package/dist/video-providers/index.js +14 -0
- package/dist/video-providers/minimax.d.ts +25 -0
- package/dist/video-providers/minimax.js +63 -0
- package/dist/video-providers/reference-encoding.d.ts +4 -0
- package/dist/video-providers/reference-encoding.js +11 -0
- package/dist/video-providers/types.d.ts +28 -0
- package/dist/video-providers/types.js +1 -0
- package/dist/video-providers/unified.d.ts +18 -0
- package/dist/video-providers/unified.js +52 -0
- package/docs/api/README.md +16 -0
- package/docs/api/app-media-contracts.md +507 -0
- package/docs/api/browser.md +126 -0
- package/docs/api/cloudflare.md +678 -0
- package/docs/api/effect.md +39 -0
- package/docs/api/index.md +1775 -0
- package/docs/api/modules.md +19 -0
- package/docs/api/provenance-export.md +114 -0
- package/docs/api/server.md +1672 -0
- package/package.json +110 -3
package/dist/browser.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { mediaFailureSchema, readMediaFailure } from "./browser-failure.js";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type MediaAiResult, type MediaGenerationSettings, type MediaProvider, type MediaReferenceBytes } from "../index.js";
|
|
2
|
+
/** Workers AI results vary by model; the provider decodes them at this boundary. */
|
|
3
|
+
export type MediaAiPort = {
|
|
4
|
+
/** Dispatch to the native Workers AI binding. */
|
|
5
|
+
run(model: string, input: Parameters<Ai["run"]>[1], options?: AiOptions): Promise<MediaAiResult>;
|
|
6
|
+
};
|
|
7
|
+
export declare function estimate(settings: MediaGenerationSettings, references: readonly MediaReferenceBytes[]): number;
|
|
8
|
+
export declare function generate(ai: MediaAiPort, settings: MediaGenerationSettings, references: readonly MediaReferenceBytes[], signal?: AbortSignal, options?: AiOptions, fetchResult?: typeof fetch, outputFormat?: "png" | "jpeg" | "webp"): Promise<{
|
|
9
|
+
bytes: Uint8Array;
|
|
10
|
+
format: string;
|
|
11
|
+
}>;
|
|
12
|
+
/** Media's provider port on the Workers AI binding, with fixed options such as the AI Gateway route. */
|
|
13
|
+
export declare function createWorkersAiMediaProvider(ai: MediaAiPort, options?: AiOptions): MediaProvider;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import * as Redacted from "effect/Redacted";
|
|
3
|
+
import { buildProviderInput, getMediaModel, estimateImageMilliunits, normalizeMediaProviderOutput, mediaProviderWireSchema, } from "../index.js";
|
|
4
|
+
import { MediaExecutionError, MediaSafetyFilterRefusal, } from "../execution-error.js";
|
|
5
|
+
import { estimateGatewayMilliunits, gatewayInput, isGatewayModel, } from "../gateway-models.js";
|
|
6
|
+
import { GATEWAY_VIDEO_MAX_BYTES, gatewayOutputSchema, isMp4, } from "../gateway-wire.js";
|
|
7
|
+
import { videoProviderAdapters } from "../video-providers/index.js";
|
|
8
|
+
import { readResponseBytes } from "../index.js";
|
|
9
|
+
export function estimate(settings, references) {
|
|
10
|
+
const params = {
|
|
11
|
+
width: settings.width,
|
|
12
|
+
height: settings.height,
|
|
13
|
+
references: references.map(({ width, height }) => ({ width, height })),
|
|
14
|
+
};
|
|
15
|
+
if (settings.steps !== undefined)
|
|
16
|
+
params.steps = settings.steps;
|
|
17
|
+
try {
|
|
18
|
+
if (isGatewayModel(settings.model))
|
|
19
|
+
return estimateGatewayMilliunits(settings);
|
|
20
|
+
return estimateImageMilliunits(settings.model, params);
|
|
21
|
+
}
|
|
22
|
+
catch (cause) {
|
|
23
|
+
throw new MediaExecutionError("estimate", { cause });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// Workers AI prefixes upstream error messages with its numeric internal code.
|
|
27
|
+
// Parse only that bounded identifier; the remaining message is never logged.
|
|
28
|
+
const providerFailureCode = z
|
|
29
|
+
.string()
|
|
30
|
+
.regex(/^\d{4,5}: /u)
|
|
31
|
+
.transform((message) => Number(message.split(":", 1)[0]))
|
|
32
|
+
.nullable()
|
|
33
|
+
.catch(null);
|
|
34
|
+
const providerFailureCategory = z
|
|
35
|
+
.enum([
|
|
36
|
+
"InferenceUpstreamError",
|
|
37
|
+
"AiInternalError",
|
|
38
|
+
"TypeError",
|
|
39
|
+
"AbortError",
|
|
40
|
+
"TimeoutError",
|
|
41
|
+
"Error",
|
|
42
|
+
])
|
|
43
|
+
.catch("Error");
|
|
44
|
+
// Recognize the refusal, not the generic Workers AI error number or prompt text.
|
|
45
|
+
const safetyFilterMessage = z.union([
|
|
46
|
+
z
|
|
47
|
+
.string()
|
|
48
|
+
.regex(/(?:^|:\s)(?:AiError:\s)?Input prompt contains NSFW content\b/iu),
|
|
49
|
+
// Veo's documented input-policy refusal; keep its details private as well.
|
|
50
|
+
// https://docs.cloud.google.com/vertex-ai/generative-ai/docs/video/responsible-ai-and-usage-guidelines
|
|
51
|
+
z.string().regex(/\bit might violate our policies\b/iu),
|
|
52
|
+
]);
|
|
53
|
+
/** Encode multipart once with the platform's boundary, as required by AI.run. */
|
|
54
|
+
function workersAiInput(payload) {
|
|
55
|
+
if (payload.transport === "json")
|
|
56
|
+
return { ...payload.body };
|
|
57
|
+
const encoded = new Response(payload.body);
|
|
58
|
+
return {
|
|
59
|
+
multipart: {
|
|
60
|
+
body: encoded.body,
|
|
61
|
+
contentType: encoded.headers.get("Content-Type"),
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/** Retain the original provider failure alongside its bounded code and category. */
|
|
66
|
+
function providerFailure(model, cause) {
|
|
67
|
+
// Workers correlates this structured event with the request trace. Never log
|
|
68
|
+
// the provider message/body: either can contain customer prompt content.
|
|
69
|
+
console.error("Media provider request failed", {
|
|
70
|
+
model,
|
|
71
|
+
providerCode: providerFailureCode.parse(cause instanceof Error ? cause.message : undefined),
|
|
72
|
+
category: providerFailureCategory.parse(cause instanceof Error ? cause.name : undefined),
|
|
73
|
+
}, Redacted.make(cause));
|
|
74
|
+
return cause instanceof Error &&
|
|
75
|
+
safetyFilterMessage.safeParse(cause.message).success
|
|
76
|
+
? new MediaSafetyFilterRefusal({ cause })
|
|
77
|
+
: new MediaExecutionError("provider", { cause });
|
|
78
|
+
}
|
|
79
|
+
/** Run a gateway model and fetch its result URL within the byte bound. */
|
|
80
|
+
async function generateThroughGateway(ai, settings, references, signal, options, fetchResult, outputFormat) {
|
|
81
|
+
let providerValue;
|
|
82
|
+
try {
|
|
83
|
+
const input = gatewayInput(settings, references, outputFormat);
|
|
84
|
+
providerValue = await ai.run(settings.model, input, signal === undefined ? options : { ...options, signal });
|
|
85
|
+
}
|
|
86
|
+
catch (cause) {
|
|
87
|
+
throw providerFailure(settings.model, cause);
|
|
88
|
+
}
|
|
89
|
+
const model = getMediaModel(settings.model);
|
|
90
|
+
const video = model?.kind === "video";
|
|
91
|
+
try {
|
|
92
|
+
if (video) {
|
|
93
|
+
if (model.video === null)
|
|
94
|
+
throw new Error("video model is missing its catalog video metadata");
|
|
95
|
+
const url = videoProviderAdapters[model.video.provider].decodeResultUrl(providerValue);
|
|
96
|
+
const response = await fetchResult(url, { signal: signal ?? null });
|
|
97
|
+
if (!response.ok)
|
|
98
|
+
throw new Error("gateway video result unavailable");
|
|
99
|
+
const bytes = await readResponseBytes(response, GATEWAY_VIDEO_MAX_BYTES);
|
|
100
|
+
if (!isMp4(bytes))
|
|
101
|
+
throw new Error("gateway video is not an MP4");
|
|
102
|
+
return { bytes, format: "mp4" };
|
|
103
|
+
}
|
|
104
|
+
const parsed = gatewayOutputSchema.parse(providerValue);
|
|
105
|
+
return await normalizeMediaProviderOutput(mediaProviderWireSchema.parse({
|
|
106
|
+
images: [{ url: parsed.result.image }],
|
|
107
|
+
}), { fetchUrl: (url) => fetchResult(url, { signal: signal ?? null }) });
|
|
108
|
+
}
|
|
109
|
+
catch (cause) {
|
|
110
|
+
throw new MediaExecutionError("normalization", { cause });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
export async function generate(ai, settings, references, signal, options, fetchResult = (input, init) => fetch(input, init), outputFormat) {
|
|
114
|
+
if (isGatewayModel(settings.model))
|
|
115
|
+
return generateThroughGateway(ai, settings, references, signal, options, fetchResult, outputFormat);
|
|
116
|
+
let providerInput;
|
|
117
|
+
try {
|
|
118
|
+
providerInput = buildProviderInput(settings, references);
|
|
119
|
+
}
|
|
120
|
+
catch (cause) {
|
|
121
|
+
throw new MediaExecutionError("normalization", { cause });
|
|
122
|
+
}
|
|
123
|
+
let providerValue;
|
|
124
|
+
try {
|
|
125
|
+
providerValue = await ai.run(settings.model, workersAiInput(providerInput), signal === undefined ? options : { ...options, signal });
|
|
126
|
+
}
|
|
127
|
+
catch (cause) {
|
|
128
|
+
throw providerFailure(settings.model, cause);
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
return await normalizeMediaProviderOutput(mediaProviderWireSchema.parse(providerValue));
|
|
132
|
+
}
|
|
133
|
+
catch (cause) {
|
|
134
|
+
throw new MediaExecutionError("normalization", { cause });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/** Media's provider port on the Workers AI binding, with fixed options such as the AI Gateway route. */
|
|
138
|
+
export function createWorkersAiMediaProvider(ai, options) {
|
|
139
|
+
return (settings, references, signal, outputFormat) => generate(ai, settings, references, signal, options, undefined, outputFormat);
|
|
140
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type ImageViewFitter, type ReferenceImageFitter } from "../index.js";
|
|
2
|
+
/** Only image metadata and the bounded resize chain are consumed by Media. */
|
|
3
|
+
export type MediaImagePort = {
|
|
4
|
+
/** Read decoded image metadata through the Images binding. */
|
|
5
|
+
info(input: ReadableStream<Uint8Array>): ReturnType<ImagesBinding["info"]>;
|
|
6
|
+
/** Start the native resize and encode chain. */
|
|
7
|
+
input(input: ReadableStream<Uint8Array>): {
|
|
8
|
+
/** Apply the native scale-down transform. */
|
|
9
|
+
transform(options: {
|
|
10
|
+
/** Image width in pixels. */
|
|
11
|
+
width: number;
|
|
12
|
+
/** Image height in pixels. */
|
|
13
|
+
height: number;
|
|
14
|
+
fit: "scale-down";
|
|
15
|
+
}): {
|
|
16
|
+
/** Encode the transformed image through the native Images binding. */
|
|
17
|
+
output(options: {
|
|
18
|
+
/** Actual output encoding. */
|
|
19
|
+
format: "image/png";
|
|
20
|
+
} | {
|
|
21
|
+
/** Actual output encoding. */
|
|
22
|
+
format: "image/jpeg";
|
|
23
|
+
quality: number;
|
|
24
|
+
}): Promise<{
|
|
25
|
+
/** Provider response or encoded image response. */
|
|
26
|
+
response(): Response;
|
|
27
|
+
}>;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Media's reference preparation on the Images binding: decode the real bytes,
|
|
33
|
+
* scale an oversized image down to MAX_REFERENCE_DIMENSION as PNG, then decode
|
|
34
|
+
* the output so its metadata describes the bytes actually sent.
|
|
35
|
+
*/
|
|
36
|
+
export declare function createImagesReferenceFitter(images: MediaImagePort): ReferenceImageFitter;
|
|
37
|
+
/**
|
|
38
|
+
* Fit one image for a model to look at on the Images binding: always
|
|
39
|
+
* re-encode it as JPEG (quality 80), scaled down within `maxDimension`, and
|
|
40
|
+
* decode the output so its metadata describes the bytes actually returned.
|
|
41
|
+
*/
|
|
42
|
+
export declare function createImagesViewFitter(images: MediaImagePort): ImageViewFitter;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { MAX_REFERENCE_BYTES, MAX_REFERENCE_DIMENSION, MediaSettingsError, viewDimensionSchema, } from "../index.js";
|
|
3
|
+
/** The retained `read_image` encoding: JPEG at quality 80. */
|
|
4
|
+
const VIEW_JPEG_QUALITY = 80;
|
|
5
|
+
const metadata = z.object({
|
|
6
|
+
format: z.enum(["image/png", "image/jpeg", "image/gif", "image/webp"]),
|
|
7
|
+
width: z.number().int().positive(),
|
|
8
|
+
height: z.number().int().positive(),
|
|
9
|
+
});
|
|
10
|
+
const formats = {
|
|
11
|
+
"image/png": "png",
|
|
12
|
+
"image/jpeg": "jpeg",
|
|
13
|
+
"image/gif": "gif",
|
|
14
|
+
"image/webp": "webp",
|
|
15
|
+
};
|
|
16
|
+
const stream = (bytes) => new ReadableStream({
|
|
17
|
+
start(controller) {
|
|
18
|
+
controller.enqueue(bytes.slice());
|
|
19
|
+
controller.close();
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
/**
|
|
23
|
+
* Media's reference preparation on the Images binding: decode the real bytes,
|
|
24
|
+
* scale an oversized image down to MAX_REFERENCE_DIMENSION as PNG, then decode
|
|
25
|
+
* the output so its metadata describes the bytes actually sent.
|
|
26
|
+
*/
|
|
27
|
+
export function createImagesReferenceFitter(images) {
|
|
28
|
+
return async (input) => {
|
|
29
|
+
let bytes = input;
|
|
30
|
+
let info;
|
|
31
|
+
try {
|
|
32
|
+
info = metadata.parse(await images.info(stream(bytes)));
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// The Images failure detail is never surfaced; the caller's bytes are.
|
|
36
|
+
throw new MediaSettingsError("reference image is not a supported image");
|
|
37
|
+
}
|
|
38
|
+
if (info.width > MAX_REFERENCE_DIMENSION ||
|
|
39
|
+
info.height > MAX_REFERENCE_DIMENSION) {
|
|
40
|
+
const output = await images
|
|
41
|
+
.input(stream(bytes))
|
|
42
|
+
.transform({
|
|
43
|
+
width: MAX_REFERENCE_DIMENSION,
|
|
44
|
+
height: MAX_REFERENCE_DIMENSION,
|
|
45
|
+
fit: "scale-down",
|
|
46
|
+
})
|
|
47
|
+
.output({
|
|
48
|
+
format: "image/png",
|
|
49
|
+
});
|
|
50
|
+
bytes = new Uint8Array(await output.response().arrayBuffer());
|
|
51
|
+
if (bytes.byteLength === 0 || bytes.byteLength > MAX_REFERENCE_BYTES)
|
|
52
|
+
throw new MediaSettingsError("reference image exceeds the byte limit");
|
|
53
|
+
info = metadata.parse(await images.info(stream(bytes)));
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
bytes,
|
|
57
|
+
format: formats[info.format],
|
|
58
|
+
width: info.width,
|
|
59
|
+
height: info.height,
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Fit one image for a model to look at on the Images binding: always
|
|
65
|
+
* re-encode it as JPEG (quality 80), scaled down within `maxDimension`, and
|
|
66
|
+
* decode the output so its metadata describes the bytes actually returned.
|
|
67
|
+
*/
|
|
68
|
+
export function createImagesViewFitter(images) {
|
|
69
|
+
return async (input, maxDimension) => {
|
|
70
|
+
const bound = viewDimensionSchema.safeParse(maxDimension);
|
|
71
|
+
if (!bound.success)
|
|
72
|
+
throw new MediaSettingsError("view dimension is out of bounds");
|
|
73
|
+
try {
|
|
74
|
+
metadata.parse(await images.info(stream(input)));
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// The Images failure detail is never surfaced.
|
|
78
|
+
throw new MediaSettingsError("image is not a supported image");
|
|
79
|
+
}
|
|
80
|
+
const output = await images
|
|
81
|
+
.input(stream(input))
|
|
82
|
+
.transform({
|
|
83
|
+
width: bound.data,
|
|
84
|
+
height: bound.data,
|
|
85
|
+
fit: "scale-down",
|
|
86
|
+
})
|
|
87
|
+
.output({
|
|
88
|
+
format: "image/jpeg",
|
|
89
|
+
quality: VIEW_JPEG_QUALITY,
|
|
90
|
+
});
|
|
91
|
+
const bytes = new Uint8Array(await output.response().arrayBuffer());
|
|
92
|
+
if (bytes.byteLength === 0 || bytes.byteLength > MAX_REFERENCE_BYTES)
|
|
93
|
+
throw new MediaSettingsError("image exceeds the byte limit");
|
|
94
|
+
const info = metadata.parse(await images.info(stream(bytes)));
|
|
95
|
+
return {
|
|
96
|
+
bytes,
|
|
97
|
+
format: formats[info.format],
|
|
98
|
+
width: info.width,
|
|
99
|
+
height: info.height,
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { MediaOperationDO, type MediaOperationCloudflareEnvironment, type MediaOperationCoordinates, type MediaOperationRequest, } from "./media-operation-do.js";
|
|
2
|
+
export { createWorkersAiMediaProvider, type MediaAiPort, } from "./cloudflare/provider.js";
|
|
3
|
+
export type { MediaAiResult } from "./index.js";
|
|
4
|
+
export { createImagesReferenceFitter, createImagesViewFitter, type MediaImagePort, } from "./cloudflare/reference-images.js";
|
|
5
|
+
export { MediaOperationOwner } from "./media-operation-owner.js";
|
|
6
|
+
export type { MediaOperationContext, MediaOperationStorage, MediaOperationBucket, MediaOperationObject, } from "./media-operation-storage.js";
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { MediaOperationDO, } from "./media-operation-do.js";
|
|
2
|
+
export { createWorkersAiMediaProvider, } from "./cloudflare/provider.js";
|
|
3
|
+
export { createImagesReferenceFitter, createImagesViewFitter, } from "./cloudflare/reference-images.js";
|
|
4
|
+
export { MediaOperationOwner } from "./media-operation-owner.js";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Dimensions and optional settings used by the deterministic image quote. */
|
|
2
|
+
export type ImageParams = {
|
|
3
|
+
/** Output height in pixels. */
|
|
4
|
+
readonly height: number;
|
|
5
|
+
/** Reference dimensions counted in the quote. */
|
|
6
|
+
readonly references?: readonly ImageReference[];
|
|
7
|
+
/** Inference steps, using catalog defaults when omitted. */
|
|
8
|
+
readonly steps?: number;
|
|
9
|
+
/** Output width in pixels. */
|
|
10
|
+
readonly width: number;
|
|
11
|
+
};
|
|
12
|
+
/** Reference-image dimensions used to estimate provider work. */
|
|
13
|
+
export type ImageReference = {
|
|
14
|
+
/** Reference height in pixels. */
|
|
15
|
+
readonly height: number;
|
|
16
|
+
/** Reference width in pixels. */
|
|
17
|
+
readonly width: number;
|
|
18
|
+
};
|
|
19
|
+
/** Estimate Workers AI neurons from a validated image request. */
|
|
20
|
+
export declare const estimateImageNeurons: (modelId: string, params: ImageParams) => number;
|
|
21
|
+
/** Quote the same estimate in milliunits (1 cent = 1,000 milliunits). */
|
|
22
|
+
export declare const estimateImageMilliunits: (modelId: string, params: ImageParams) => number;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { getMediaModel, MediaSettingsError } from "./model-catalog.js";
|
|
2
|
+
import { neuronsToMilliunits } from "./neurons.js";
|
|
3
|
+
import * as z from "zod";
|
|
4
|
+
const TILE = 512;
|
|
5
|
+
const MAX_DIMENSION = 4_096;
|
|
6
|
+
const MAX_REFERENCES = 4;
|
|
7
|
+
const imageParamsSchema = z
|
|
8
|
+
.object({
|
|
9
|
+
width: z.number().int().min(1).max(MAX_DIMENSION),
|
|
10
|
+
height: z.number().int().min(1).max(MAX_DIMENSION),
|
|
11
|
+
steps: z.number().int().min(1).optional(),
|
|
12
|
+
references: z
|
|
13
|
+
.array(z
|
|
14
|
+
.object({
|
|
15
|
+
width: z.number().int().min(1).max(MAX_DIMENSION),
|
|
16
|
+
height: z.number().int().min(1).max(MAX_DIMENSION),
|
|
17
|
+
})
|
|
18
|
+
.strict())
|
|
19
|
+
.max(MAX_REFERENCES)
|
|
20
|
+
.optional(),
|
|
21
|
+
})
|
|
22
|
+
.strict();
|
|
23
|
+
const invalid = (message) => {
|
|
24
|
+
throw new MediaSettingsError(`image cost parameters are invalid: ${message}`);
|
|
25
|
+
};
|
|
26
|
+
const tiles = (width, height) => Math.ceil(width / TILE) * Math.ceil(height / TILE);
|
|
27
|
+
const checked = (modelId, params) => {
|
|
28
|
+
const parsed = imageParamsSchema.safeParse(params);
|
|
29
|
+
if (!parsed.success)
|
|
30
|
+
return invalid("dimensions, steps, or references are invalid");
|
|
31
|
+
const candidate = parsed.data;
|
|
32
|
+
const references = candidate.references ?? [];
|
|
33
|
+
const model = getMediaModel(modelId);
|
|
34
|
+
if (model === undefined)
|
|
35
|
+
throw new MediaSettingsError(`Image cost unknown for model "${modelId}"`);
|
|
36
|
+
if (references.length > model.input.maxReferences)
|
|
37
|
+
return invalid(`${modelId} does not accept references`);
|
|
38
|
+
const normalized = {
|
|
39
|
+
width: candidate.width,
|
|
40
|
+
height: candidate.height,
|
|
41
|
+
references,
|
|
42
|
+
};
|
|
43
|
+
if (candidate.steps !== undefined)
|
|
44
|
+
return { ...normalized, steps: candidate.steps };
|
|
45
|
+
return normalized;
|
|
46
|
+
};
|
|
47
|
+
/** Estimate Workers AI neurons from a validated image request. */
|
|
48
|
+
export const estimateImageNeurons = (modelId, params) => {
|
|
49
|
+
const p = checked(modelId, params);
|
|
50
|
+
const outputTiles = tiles(p.width, p.height);
|
|
51
|
+
const inputTiles = (p.references ?? []).reduce((sum, reference) => sum + tiles(reference.width, reference.height), 0);
|
|
52
|
+
switch (modelId) {
|
|
53
|
+
case "@cf/black-forest-labs/flux-1-schnell":
|
|
54
|
+
return outputTiles * 4.8 + (p.steps ?? 4) * 9.6;
|
|
55
|
+
case "@cf/black-forest-labs/flux-2-klein-4b":
|
|
56
|
+
return inputTiles * 5.37 + outputTiles * 26.05;
|
|
57
|
+
case "@cf/black-forest-labs/flux-2-dev":
|
|
58
|
+
return (inputTiles * (p.steps ?? 20) * 19.09 +
|
|
59
|
+
outputTiles * (p.steps ?? 20) * 37.27);
|
|
60
|
+
case "@cf/black-forest-labs/flux-2-klein-9b": {
|
|
61
|
+
const outputMp = (p.width / 1024) * (p.height / 1024);
|
|
62
|
+
const outputCost = Math.min(outputMp, 1) * 1363.64 + Math.max(0, outputMp - 1) * 181.82;
|
|
63
|
+
const inputMp = (p.references ?? []).reduce((sum, reference) => sum + (reference.width / 1024) * (reference.height / 1024), 0);
|
|
64
|
+
return (outputMp < 1 ? 1363.64 : outputCost) + inputMp * 181.82;
|
|
65
|
+
}
|
|
66
|
+
case "@cf/leonardo/lucid-origin":
|
|
67
|
+
return outputTiles * 636 + (p.steps ?? 20) * 12;
|
|
68
|
+
case "@cf/leonardo/phoenix-1.0":
|
|
69
|
+
return outputTiles * 530 + (p.steps ?? 25) * 10;
|
|
70
|
+
default:
|
|
71
|
+
// checked() owns this rejection; keep the switch exhaustive at runtime.
|
|
72
|
+
throw new MediaSettingsError(`Image cost unknown for model "${modelId}"`);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
/** Quote the same estimate in milliunits (1 cent = 1,000 milliunits). */
|
|
76
|
+
export const estimateImageMilliunits = (modelId, params) => neuronsToMilliunits(estimateImageNeurons(modelId, params));
|
package/dist/effect.d.ts
ADDED
package/dist/effect.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
/** Public diagnostic labels only; never provider messages or request data. */
|
|
3
|
+
export declare const mediaFailureStageSchema: z.ZodEnum<{
|
|
4
|
+
/** Host admission before operation creation. */
|
|
5
|
+
readonly admission: "admission";
|
|
6
|
+
/** Host authority check before dispatch. */
|
|
7
|
+
readonly "generation-authorization": "generation-authorization";
|
|
8
|
+
/** Generation settings validation. */
|
|
9
|
+
readonly settings: "settings";
|
|
10
|
+
/** Reference preparation and cost estimation. */
|
|
11
|
+
readonly estimate: "estimate";
|
|
12
|
+
/** Host price admission or settlement. */
|
|
13
|
+
readonly billing: "billing";
|
|
14
|
+
/** Durable operation transition. */
|
|
15
|
+
readonly operation: "operation";
|
|
16
|
+
/** Provider dispatch. */
|
|
17
|
+
readonly provider: "provider";
|
|
18
|
+
/** Provider output decoding. */
|
|
19
|
+
readonly normalization: "normalization";
|
|
20
|
+
/** Retained output storage. */
|
|
21
|
+
readonly artifact: "artifact";
|
|
22
|
+
/** Host permission check before saving. */
|
|
23
|
+
readonly "destination-authorization": "destination-authorization";
|
|
24
|
+
/** Host destination write. */
|
|
25
|
+
readonly "destination-write": "destination-write";
|
|
26
|
+
}>;
|
|
27
|
+
/** Execution stage at which a generation failed. */
|
|
28
|
+
export type MediaFailureStage = Exclude<z.output<typeof mediaFailureStageSchema>, "admission" | "settings">;
|
|
29
|
+
/** A bounded stage marker for failures that cross the Media HTTP boundary. */
|
|
30
|
+
export declare class MediaExecutionError extends Error {
|
|
31
|
+
/** Execution stage where the failure occurred. */
|
|
32
|
+
readonly stage: MediaFailureStage;
|
|
33
|
+
constructor(
|
|
34
|
+
/** Execution stage where the failure occurred. */
|
|
35
|
+
stage: MediaFailureStage, options?: ErrorOptions);
|
|
36
|
+
}
|
|
37
|
+
/** A provider safety filter refused generation; no output was delivered. */
|
|
38
|
+
export declare class MediaSafetyFilterRefusal extends MediaExecutionError {
|
|
39
|
+
constructor(options?: ErrorOptions);
|
|
40
|
+
}
|
|
41
|
+
/** Why the host's billing owner refused to admit a new generation. */
|
|
42
|
+
export type MediaBillingRefusalCode = "insufficient_credits" | "daily_limit_reached" | "denied";
|
|
43
|
+
/** A definitive billing refusal before dispatch; nothing was generated or charged. */
|
|
44
|
+
export declare class MediaBillingRefusal extends Error {
|
|
45
|
+
/** Bounded refusal or failure reason. */
|
|
46
|
+
readonly code: MediaBillingRefusalCode;
|
|
47
|
+
constructor(
|
|
48
|
+
/** Bounded refusal or failure reason. */
|
|
49
|
+
code: MediaBillingRefusalCode);
|
|
50
|
+
}
|
|
51
|
+
/** The durable format pin prevents a mismatched file extension or retained output. */
|
|
52
|
+
export declare class MediaOutputFormatMismatch extends Error {
|
|
53
|
+
constructor();
|
|
54
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
/** Public diagnostic labels only; never provider messages or request data. */
|
|
3
|
+
export const mediaFailureStageSchema = z.enum({
|
|
4
|
+
/** Host admission before operation creation. */
|
|
5
|
+
admission: "admission",
|
|
6
|
+
/** Host authority check before dispatch. */
|
|
7
|
+
"generation-authorization": "generation-authorization",
|
|
8
|
+
/** Generation settings validation. */
|
|
9
|
+
settings: "settings",
|
|
10
|
+
/** Reference preparation and cost estimation. */
|
|
11
|
+
estimate: "estimate",
|
|
12
|
+
/** Host price admission or settlement. */
|
|
13
|
+
billing: "billing",
|
|
14
|
+
/** Durable operation transition. */
|
|
15
|
+
operation: "operation",
|
|
16
|
+
/** Provider dispatch. */
|
|
17
|
+
provider: "provider",
|
|
18
|
+
/** Provider output decoding. */
|
|
19
|
+
normalization: "normalization",
|
|
20
|
+
/** Retained output storage. */
|
|
21
|
+
artifact: "artifact",
|
|
22
|
+
/** Host permission check before saving. */
|
|
23
|
+
"destination-authorization": "destination-authorization",
|
|
24
|
+
/** Host destination write. */
|
|
25
|
+
"destination-write": "destination-write",
|
|
26
|
+
});
|
|
27
|
+
/** A bounded stage marker for failures that cross the Media HTTP boundary. */
|
|
28
|
+
export class MediaExecutionError extends Error {
|
|
29
|
+
stage;
|
|
30
|
+
constructor(
|
|
31
|
+
/** Execution stage where the failure occurred. */
|
|
32
|
+
stage, options) {
|
|
33
|
+
super(`Media ${stage} failed`, options);
|
|
34
|
+
this.stage = stage;
|
|
35
|
+
this.name = "MediaExecutionError";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** A provider safety filter refused generation; no output was delivered. */
|
|
39
|
+
export class MediaSafetyFilterRefusal extends MediaExecutionError {
|
|
40
|
+
constructor(options) {
|
|
41
|
+
super("provider", options);
|
|
42
|
+
this.name = "MediaSafetyFilterRefusal";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** A definitive billing refusal before dispatch; nothing was generated or charged. */
|
|
46
|
+
export class MediaBillingRefusal extends Error {
|
|
47
|
+
code;
|
|
48
|
+
constructor(
|
|
49
|
+
/** Bounded refusal or failure reason. */
|
|
50
|
+
code) {
|
|
51
|
+
super(`Media billing refused: ${code}`);
|
|
52
|
+
this.code = code;
|
|
53
|
+
this.name = "MediaBillingRefusal";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** The durable format pin prevents a mismatched file extension or retained output. */
|
|
57
|
+
export class MediaOutputFormatMismatch extends Error {
|
|
58
|
+
constructor() {
|
|
59
|
+
super("Media provider did not return the requested image format");
|
|
60
|
+
this.name = "MediaOutputFormatMismatch";
|
|
61
|
+
}
|
|
62
|
+
}
|
package/dist/failure.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import * as Redacted from "effect/Redacted";
|
|
2
|
+
/** Retain the original failure without printing provider or customer data. */
|
|
3
|
+
export function reportCatchFailure(operation, cause) {
|
|
4
|
+
console.error("Media service operation failed", {
|
|
5
|
+
operation,
|
|
6
|
+
errorKind: cause instanceof Error ? "error" : "non-error",
|
|
7
|
+
}, Redacted.make(cause));
|
|
8
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type MediaGenerationSettings, type MediaReferenceBytes } from "./index.js";
|
|
2
|
+
import type { MediaVideoModelId } from "./model-catalog.js";
|
|
3
|
+
/** Gateway images and the verified catalog's video families share Unified billing. */
|
|
4
|
+
export type GatewayModelId = "openai/gpt-image-2" | MediaVideoModelId;
|
|
5
|
+
export declare const isGatewayModel: (id: string) => id is GatewayModelId;
|
|
6
|
+
/** What one generation bills: whole images, or seconds of video. */
|
|
7
|
+
export type MediaUnits = Readonly<{
|
|
8
|
+
/** One image or one whole video second. */
|
|
9
|
+
unit: "image" | "video_second";
|
|
10
|
+
/** Number of delivered or admitted units. */
|
|
11
|
+
units: number;
|
|
12
|
+
}>;
|
|
13
|
+
/** Billing units for validated settings; video bills its requested length. */
|
|
14
|
+
export declare function mediaUnits(settings: MediaGenerationSettings): MediaUnits;
|
|
15
|
+
/** Milliunits (1 cent = 1,000) of provider cost, rounded up. */
|
|
16
|
+
export declare function estimateGatewayMilliunits(settings: MediaGenerationSettings): number;
|
|
17
|
+
/** The provider JSON body for one validated gateway generation. */
|
|
18
|
+
export declare function gatewayInput(settings: MediaGenerationSettings, references?: readonly MediaReferenceBytes[], outputFormat?: "png" | "jpeg" | "webp"): import("./video-providers/minimax.js").MinimaxGatewayInput | import("./video-providers/unified.js").UnifiedGatewayInput | {
|
|
19
|
+
prompt: string;
|
|
20
|
+
size: string;
|
|
21
|
+
quality: string;
|
|
22
|
+
output_format: "png" | "jpeg" | "webp";
|
|
23
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { getMediaModel, MediaSettingsError } from "./model-catalog.js";
|
|
2
|
+
import { validateMediaGenerationSettings, estimateVideoMilliunits, } from "./index.js";
|
|
3
|
+
import { videoProviderAdapters, } from "./video-providers/index.js";
|
|
4
|
+
export const isGatewayModel = (id) => id === "openai/gpt-image-2" || getMediaModel(id)?.kind === "video";
|
|
5
|
+
/** Billing units for validated settings; video bills its requested length. */
|
|
6
|
+
export function mediaUnits(settings) {
|
|
7
|
+
const model = getMediaModel(settings.model);
|
|
8
|
+
if (model === undefined)
|
|
9
|
+
throw new MediaSettingsError("billing units require a known Media model");
|
|
10
|
+
if (model.kind === "video") {
|
|
11
|
+
const checked = validateMediaGenerationSettings({
|
|
12
|
+
...settings,
|
|
13
|
+
references: settings.references?.map((reference) => ({ ...reference })),
|
|
14
|
+
});
|
|
15
|
+
if (checked.durationSeconds === undefined)
|
|
16
|
+
throw new MediaSettingsError("video settings need a duration");
|
|
17
|
+
return { unit: "video_second", units: checked.durationSeconds };
|
|
18
|
+
}
|
|
19
|
+
return { unit: "image", units: 1 };
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* GPT Image 2 output tokens per image at `medium` quality. OpenAI publishes
|
|
23
|
+
* this table for its gpt-image models; PRICE DECISION PENDING (Levi): confirm
|
|
24
|
+
* against the first live gateway logs, which carry the real token counts.
|
|
25
|
+
*/
|
|
26
|
+
const gptImageOutputTokens = new Map([
|
|
27
|
+
["1024x1024", 1_056],
|
|
28
|
+
["1024x1536", 1_584],
|
|
29
|
+
["1536x1024", 1_568],
|
|
30
|
+
]);
|
|
31
|
+
/** USD per million tokens: text input and image output. */
|
|
32
|
+
const gptImageRates = { input: 5, outputImage: 30 };
|
|
33
|
+
const sizeOf = (settings) => `${String(settings.width)}x${String(settings.height)}`;
|
|
34
|
+
/** Milliunits (1 cent = 1,000) of provider cost, rounded up. */
|
|
35
|
+
export function estimateGatewayMilliunits(settings) {
|
|
36
|
+
if (settings.model === "openai/gpt-image-2") {
|
|
37
|
+
const output = gptImageOutputTokens.get(sizeOf(settings));
|
|
38
|
+
if (output === undefined)
|
|
39
|
+
throw new MediaSettingsError("unsupported GPT Image 2 size");
|
|
40
|
+
// A bound on prompt tokens: at most one token per character.
|
|
41
|
+
const input = settings.prompt.length;
|
|
42
|
+
// tokens * USD/M * 100,000 milliunits/USD / 1,000,000 = tokens * rate / 10
|
|
43
|
+
return Math.ceil((output * gptImageRates.outputImage + input * gptImageRates.input) / 10);
|
|
44
|
+
}
|
|
45
|
+
if (getMediaModel(settings.model)?.kind === "video")
|
|
46
|
+
return estimateVideoMilliunits(settings);
|
|
47
|
+
throw new MediaSettingsError(`${settings.model} is not a gateway model`);
|
|
48
|
+
}
|
|
49
|
+
/** The provider JSON body for one validated gateway generation. */
|
|
50
|
+
export function gatewayInput(settings, references = [], outputFormat = "png") {
|
|
51
|
+
if (settings.model === "openai/gpt-image-2")
|
|
52
|
+
return {
|
|
53
|
+
prompt: settings.prompt,
|
|
54
|
+
size: sizeOf(settings),
|
|
55
|
+
quality: "medium",
|
|
56
|
+
output_format: outputFormat,
|
|
57
|
+
};
|
|
58
|
+
const checked = validateMediaGenerationSettings({
|
|
59
|
+
...settings,
|
|
60
|
+
references: settings.references?.map((reference) => ({ ...reference })),
|
|
61
|
+
});
|
|
62
|
+
const model = getMediaModel(checked.model);
|
|
63
|
+
if (model?.video == null ||
|
|
64
|
+
checked.resolution === undefined ||
|
|
65
|
+
checked.aspectRatio === undefined ||
|
|
66
|
+
checked.durationSeconds === undefined)
|
|
67
|
+
throw new MediaSettingsError("not a video gateway model");
|
|
68
|
+
if (references.length !== (checked.references?.length ?? 0))
|
|
69
|
+
throw new MediaSettingsError("resolved video references must match every admitted reference");
|
|
70
|
+
const videoSettings = {
|
|
71
|
+
...checked,
|
|
72
|
+
resolution: checked.resolution,
|
|
73
|
+
aspectRatio: checked.aspectRatio,
|
|
74
|
+
durationSeconds: checked.durationSeconds,
|
|
75
|
+
};
|
|
76
|
+
return videoProviderAdapters[model.video.provider].buildInput(videoSettings, references, model);
|
|
77
|
+
}
|