@fungi.computer/darkroom 0.3.1 → 0.4.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/README.md +36 -11
- package/dist/app-media-contracts.d.ts +307 -28
- package/dist/app-media-contracts.js +26 -1
- package/dist/browser-failure.d.ts +5 -7
- package/dist/browser-failure.js +6 -1
- package/dist/cloudflare/provider.d.ts +23 -1
- package/dist/cloudflare/provider.js +65 -12
- package/dist/cloudflare/reference-images.d.ts +4 -4
- package/dist/cloudflare/reference-images.js +8 -11
- package/dist/execution-error.d.ts +4 -0
- package/dist/execution-error.js +3 -0
- package/dist/index.d.ts +11 -9
- package/dist/index.js +12 -10
- package/dist/media-binding.d.ts +5 -2
- package/dist/media-binding.js +7 -2
- package/dist/media-generation.js +2 -2
- package/dist/media-operation.js +32 -13
- package/dist/reference-image-size.d.ts +10 -0
- package/dist/reference-image-size.js +37 -0
- package/dist/server.d.ts +3 -2
- package/dist/server.js +23 -4
- package/docs/api/app-media-contracts.md +213 -25
- package/docs/api/browser.md +9 -34
- package/docs/api/cloudflare.md +6 -6
- package/docs/api/index.md +73 -60
- package/docs/api/server.md +41 -5
- package/package.json +2 -2
|
@@ -1,12 +1,31 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import * as Redacted from "effect/Redacted";
|
|
3
|
-
import { buildProviderInput, getMediaModel, estimateImageMilliunits, normalizeMediaProviderOutput, mediaProviderWireSchema, } from "../index.js";
|
|
3
|
+
import { buildProviderInput, getMediaModel, estimateImageMilliunits, normalizeMediaProviderOutput, mediaProviderWireSchema, validateMediaGenerationSettings, } from "../index.js";
|
|
4
4
|
import { MediaExecutionError, MediaSafetyFilterRefusal, } from "../execution-error.js";
|
|
5
5
|
import { decodeMediaProviderErrorCategory, } from "@fungi.computer/agar/errors";
|
|
6
6
|
import { estimateGatewayMilliunits, gatewayInput, isGatewayModel, } from "../gateway-models.js";
|
|
7
7
|
import { GATEWAY_VIDEO_MAX_BYTES, gatewayOutputSchema, isMp4, } from "../gateway-wire.js";
|
|
8
8
|
import { videoProviderAdapters } from "../video-providers/index.js";
|
|
9
9
|
import { readResponseBytes } from "../index.js";
|
|
10
|
+
/** Quote the catalog defaults through the exact estimator used before dispatch. */
|
|
11
|
+
export function estimateMediaModel(model) {
|
|
12
|
+
const settings = validateMediaGenerationSettings({
|
|
13
|
+
model: model.id,
|
|
14
|
+
prompt: "x",
|
|
15
|
+
width: model.input.defaultWidth,
|
|
16
|
+
height: model.input.defaultHeight,
|
|
17
|
+
});
|
|
18
|
+
return {
|
|
19
|
+
/** An indicative quote, not an admitted generation receipt. */
|
|
20
|
+
kind: "estimate",
|
|
21
|
+
/** Provider cost before the host's billing policy. */
|
|
22
|
+
basis: "provider-cost",
|
|
23
|
+
/** Catalog defaults, a one-character prompt and no references. */
|
|
24
|
+
settings,
|
|
25
|
+
/** Estimated provider milliunits of a cent for these exact settings. */
|
|
26
|
+
costMilliunits: estimate(settings, []),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
10
29
|
export function estimate(settings, references) {
|
|
11
30
|
const params = {
|
|
12
31
|
width: settings.width,
|
|
@@ -25,15 +44,21 @@ export function estimate(settings, references) {
|
|
|
25
44
|
}
|
|
26
45
|
}
|
|
27
46
|
// Workers AI prefixes upstream error messages with its numeric internal code.
|
|
28
|
-
// Parse only that bounded identifier;
|
|
47
|
+
// Parse only that bounded identifier; printable prose is separately decoded.
|
|
29
48
|
const providerFailureCode = z.lazy(() => z
|
|
30
49
|
.string()
|
|
31
50
|
.regex(/^\d{4,5}: /u)
|
|
32
51
|
.transform((message) => Number(message.split(":", 1)[0]))
|
|
33
52
|
.nullable()
|
|
34
53
|
.catch(null));
|
|
54
|
+
// Observed in a real Flux replay: HTTP 400/code 3030. Match the complete
|
|
55
|
+
// bounded refusal, not 3030 itself; the code is not documented as transient.
|
|
56
|
+
const outputFlaggedMessage = z
|
|
57
|
+
.string()
|
|
58
|
+
.regex(/^(?:\d{4,5}: )?(?:AiError: ){0,3}Your output has been flagged\. Please choose another prompt \/ input image combination \([0-9a-f-]{36}\)$/u);
|
|
35
59
|
// Recognize the refusal, not the generic Workers AI error number or prompt text.
|
|
36
60
|
const safetyFilterMessage = z.lazy(() => z.union([
|
|
61
|
+
outputFlaggedMessage,
|
|
37
62
|
z
|
|
38
63
|
.string()
|
|
39
64
|
.regex(/(?:^|:\s)(?:AiError:\s)?Input prompt contains NSFW content\b/iu),
|
|
@@ -78,16 +103,28 @@ function routedOptions(options, signal, input) {
|
|
|
78
103
|
}
|
|
79
104
|
/** Retain the original provider failure alongside its bounded code and category. */
|
|
80
105
|
function providerFailure(model, cause) {
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
//
|
|
86
|
-
//
|
|
106
|
+
const category = decodeMediaProviderErrorCategory(cause instanceof Error ? cause.name : undefined);
|
|
107
|
+
const code = providerFailureCode.parse(cause instanceof Error ? cause.message : undefined);
|
|
108
|
+
// Observed 2026-10-27 in a real GPT Image 2 call from the Prints UI:
|
|
109
|
+
// "AiGatewayError: 2021: Insufficient AI Gateway credits" (no charge).
|
|
110
|
+
// Sources: ~/Botanical-cto/receipts/prints-e2e/REPORT.md:51 ("Follow-up 2")
|
|
111
|
+
// and ~/Botanical-cto/receipts/prints-e2e/gwfix/results.json (02:01 local;
|
|
112
|
+
// call at 2026-10-27T01:01:04.296Z). The error had only name and message;
|
|
113
|
+
// no HTTP status was recorded. This is observed wording, not a general
|
|
114
|
+
// Cloudflare code guarantee. Keep numeric classification at this owner.
|
|
115
|
+
const provider = code === 2021
|
|
116
|
+
? { category, code, reason: "credits_exhausted" }
|
|
117
|
+
: { category, code };
|
|
118
|
+
const safeMessage = outputFlaggedMessage.safeParse(cause instanceof Error ? cause.message : undefined);
|
|
119
|
+
// The maintained Workers AI errors expose name/message, not a Response or
|
|
120
|
+
// cause chain. Only the complete, bounded refusal grammar above is safe to
|
|
121
|
+
// print. Unmodeled upstream prose/bodies can echo private request content;
|
|
122
|
+
// retain the original error (including any nested details) only as redacted.
|
|
87
123
|
console.error("Media provider request failed", {
|
|
88
124
|
model,
|
|
89
125
|
providerCode: provider.code,
|
|
90
126
|
category: provider.category,
|
|
127
|
+
providerMessage: safeMessage.success ? safeMessage.data : undefined,
|
|
91
128
|
}, Redacted.make(cause));
|
|
92
129
|
return cause instanceof Error &&
|
|
93
130
|
safetyFilterMessage.safeParse(cause.message).success
|
|
@@ -145,15 +182,31 @@ export async function generate(ai, settings, references, signal, options, fetchR
|
|
|
145
182
|
catch (cause) {
|
|
146
183
|
throw new MediaExecutionError("normalization", { cause });
|
|
147
184
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
providerValue = await ai.run(settings.model, workersAiInput(providerInput), routedOptions(options, signal, {
|
|
185
|
+
const invoke = async () => {
|
|
186
|
+
return ai.run(settings.model, workersAiInput(providerInput), routedOptions(options, signal, {
|
|
151
187
|
stream: providerInput.transport === "multipart",
|
|
152
188
|
images: references.length,
|
|
153
189
|
}));
|
|
190
|
+
};
|
|
191
|
+
let providerValue;
|
|
192
|
+
try {
|
|
193
|
+
providerValue = await invoke();
|
|
154
194
|
}
|
|
155
195
|
catch (cause) {
|
|
156
|
-
|
|
196
|
+
// Only 3040 is documented as capacity temporarily exceeded:
|
|
197
|
+
// https://developers.cloudflare.com/workers-ai/platform/errors/
|
|
198
|
+
// One product-call retry, never a test retry. Rebuild the multipart body
|
|
199
|
+
// inside invoke: a failed binding call may already have consumed it.
|
|
200
|
+
if (providerFailureCode.parse(cause instanceof Error ? cause.message : undefined) !== 3040 ||
|
|
201
|
+
signal?.aborted)
|
|
202
|
+
throw providerFailure(settings.model, cause);
|
|
203
|
+
try {
|
|
204
|
+
providerValue = await invoke();
|
|
205
|
+
}
|
|
206
|
+
catch (cause) {
|
|
207
|
+
// One terminal diagnostic for the product call, not one per attempt.
|
|
208
|
+
throw providerFailure(settings.model, cause);
|
|
209
|
+
}
|
|
157
210
|
}
|
|
158
211
|
try {
|
|
159
212
|
return await normalizeMediaProviderOutput(mediaProviderWireSchema.parse(providerValue));
|
|
@@ -5,13 +5,13 @@ export type MediaImagePort = {
|
|
|
5
5
|
info(input: ReadableStream<Uint8Array>): ReturnType<ImagesBinding["info"]>;
|
|
6
6
|
/** Start the native resize and encode chain. */
|
|
7
7
|
input(input: ReadableStream<Uint8Array>): {
|
|
8
|
-
/** Apply the native
|
|
8
|
+
/** Apply the native aspect-preserving resize. */
|
|
9
9
|
transform(options: {
|
|
10
10
|
/** Image width in pixels. */
|
|
11
11
|
width: number;
|
|
12
12
|
/** Image height in pixels. */
|
|
13
13
|
height: number;
|
|
14
|
-
fit: "scale-down";
|
|
14
|
+
fit: "scale-down" | "contain";
|
|
15
15
|
}): {
|
|
16
16
|
/** Encode the transformed image through the native Images binding. */
|
|
17
17
|
output(options: {
|
|
@@ -30,8 +30,8 @@ export type MediaImagePort = {
|
|
|
30
30
|
};
|
|
31
31
|
/**
|
|
32
32
|
* Media's reference preparation on the Images binding: decode the real bytes,
|
|
33
|
-
*
|
|
34
|
-
*
|
|
33
|
+
* fit the selected model's reference size as PNG, then decode the output so
|
|
34
|
+
* its metadata describes the bytes actually sent.
|
|
35
35
|
*/
|
|
36
36
|
export declare function createImagesReferenceFitter(images: MediaImagePort): ReferenceImageFitter;
|
|
37
37
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { MAX_REFERENCE_BYTES,
|
|
2
|
+
import { MAX_REFERENCE_BYTES, MediaSettingsError, viewDimensionSchema, } from "../index.js";
|
|
3
|
+
import { referenceImageTransform } from "../reference-image-size.js";
|
|
3
4
|
/** The retained `read_image` encoding: JPEG at quality 80. */
|
|
4
5
|
const VIEW_JPEG_QUALITY = 80;
|
|
5
6
|
const metadata = z.lazy(() => z.object({
|
|
@@ -21,11 +22,11 @@ const stream = (bytes) => new ReadableStream({
|
|
|
21
22
|
});
|
|
22
23
|
/**
|
|
23
24
|
* Media's reference preparation on the Images binding: decode the real bytes,
|
|
24
|
-
*
|
|
25
|
-
*
|
|
25
|
+
* fit the selected model's reference size as PNG, then decode the output so
|
|
26
|
+
* its metadata describes the bytes actually sent.
|
|
26
27
|
*/
|
|
27
28
|
export function createImagesReferenceFitter(images) {
|
|
28
|
-
return async (input) => {
|
|
29
|
+
return async (input, model) => {
|
|
29
30
|
let bytes = input;
|
|
30
31
|
let info;
|
|
31
32
|
try {
|
|
@@ -35,15 +36,11 @@ export function createImagesReferenceFitter(images) {
|
|
|
35
36
|
// The Images failure detail is never surfaced; the caller's bytes are.
|
|
36
37
|
throw new MediaSettingsError("reference image is not a supported image");
|
|
37
38
|
}
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
const transform = referenceImageTransform(model, info.width, info.height);
|
|
40
|
+
if (info.width !== transform.width || info.height !== transform.height) {
|
|
40
41
|
const output = await images
|
|
41
42
|
.input(stream(bytes))
|
|
42
|
-
.transform(
|
|
43
|
-
width: MAX_REFERENCE_DIMENSION,
|
|
44
|
-
height: MAX_REFERENCE_DIMENSION,
|
|
45
|
-
fit: "scale-down",
|
|
46
|
-
})
|
|
43
|
+
.transform(transform)
|
|
47
44
|
.output({
|
|
48
45
|
format: "image/png",
|
|
49
46
|
});
|
|
@@ -31,6 +31,8 @@ export type MediaFailureStage = Exclude<z.output<typeof mediaFailureStageSchema>
|
|
|
31
31
|
export type MediaExecutionErrorOptions = ErrorOptions & {
|
|
32
32
|
/** Safe provider facts, when the failure came from provider transport. */
|
|
33
33
|
readonly provider?: MediaProviderDiagnostic | undefined;
|
|
34
|
+
/** Confirmed zero-unit customer settlement, never an upstream billing claim. */
|
|
35
|
+
readonly customerCharge?: "none" | undefined;
|
|
34
36
|
};
|
|
35
37
|
/** A bounded stage marker for failures that cross the Media HTTP boundary. */
|
|
36
38
|
export declare class MediaExecutionError extends Error {
|
|
@@ -38,6 +40,8 @@ export declare class MediaExecutionError extends Error {
|
|
|
38
40
|
readonly stage: MediaFailureStage;
|
|
39
41
|
/** Safe provider facts only; the original cause remains private. */
|
|
40
42
|
readonly provider: MediaProviderDiagnostic | undefined;
|
|
43
|
+
/** Unknown unless the execution owner confirmed zero-unit settlement. */
|
|
44
|
+
readonly customerCharge: "none" | "unknown";
|
|
41
45
|
constructor(
|
|
42
46
|
/** Execution stage where the failure occurred. */
|
|
43
47
|
stage: MediaFailureStage, options?: MediaExecutionErrorOptions);
|
package/dist/execution-error.js
CHANGED
|
@@ -30,12 +30,15 @@ export class MediaExecutionError extends Error {
|
|
|
30
30
|
stage;
|
|
31
31
|
/** Safe provider facts only; the original cause remains private. */
|
|
32
32
|
provider;
|
|
33
|
+
/** Unknown unless the execution owner confirmed zero-unit settlement. */
|
|
34
|
+
customerCharge;
|
|
33
35
|
constructor(
|
|
34
36
|
/** Execution stage where the failure occurred. */
|
|
35
37
|
stage, options) {
|
|
36
38
|
super(`Media ${stage} failed`, options);
|
|
37
39
|
this.stage = stage;
|
|
38
40
|
this.name = "MediaExecutionError";
|
|
41
|
+
this.customerCharge = options?.customerCharge ?? "unknown";
|
|
39
42
|
this.provider =
|
|
40
43
|
options?.provider === undefined
|
|
41
44
|
? undefined
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
|
+
export { MAX_REFERENCE_DIMENSION } from "./reference-image-size.js";
|
|
2
3
|
import { type MediaVideoReferenceRole, type MediaVideoModelId, type MediaVideoResolution, type MediaVideoAspectRatio } from "./model-catalog.js";
|
|
3
4
|
export { mediaFailureStageSchema } from "./execution-error.js";
|
|
4
5
|
export { MAX_MEDIA_PROMPT_LENGTH, getMediaModel, MAX_REFERENCE_COUNT, listMediaModels, mediaModelInputSchema, mediaModelSchema, mediaRequestedImageFormatSchema, mediaModels, mediaModelTierSchema, MediaSettingsError, mediaVideoModelIdSchema, mediaVideoResolutionSchema, mediaVideoAspectRatioSchema, mediaVideoReferenceRoleSchema, mediaVideoFormatSchema, mediaVideoSchema, } from "./model-catalog.js";
|
|
@@ -64,10 +65,10 @@ export type MediaProvider = (settings: MediaGenerationSettings, references: read
|
|
|
64
65
|
/** Actual output encoding. */
|
|
65
66
|
format: string;
|
|
66
67
|
}>>;
|
|
67
|
-
/** A decoded reference image,
|
|
68
|
+
/** A decoded reference image, fitted for the selected model. */
|
|
68
69
|
export type FittedReferenceImage = Omit<MediaReferenceBytes, "filename">;
|
|
69
|
-
/** Decode
|
|
70
|
-
export type ReferenceImageFitter = (bytes: Uint8Array) => Promise<FittedReferenceImage>;
|
|
70
|
+
/** Decode and fit a reference: image long edge ≤512, video short edge ≥320. */
|
|
71
|
+
export type ReferenceImageFitter = (bytes: Uint8Array, model: string) => Promise<FittedReferenceImage>;
|
|
71
72
|
/** The long-edge bounds a viewer may ask for when fitting an image to look at. */
|
|
72
73
|
export declare const MIN_VIEW_DIMENSION = 256;
|
|
73
74
|
/** Maximum long-edge size accepted by an image viewer, in pixels. */
|
|
@@ -80,8 +81,6 @@ export declare const viewDimensionSchema: z.ZodLazy<z.ZodNumber>;
|
|
|
80
81
|
* a lossy JPEG, so a viewer can step the dimension down to meet a byte budget.
|
|
81
82
|
*/
|
|
82
83
|
export type ImageViewFitter = (bytes: Uint8Array, maxDimension: number) => Promise<FittedReferenceImage>;
|
|
83
|
-
/** Maximum reference width and height sent to inference, in pixels. */
|
|
84
|
-
export declare const MAX_REFERENCE_DIMENSION = 512;
|
|
85
84
|
/** Maximum decoded bytes per reference image. */
|
|
86
85
|
export declare const MAX_REFERENCE_BYTES: number;
|
|
87
86
|
/** Maximum retained still-image output bytes. */
|
|
@@ -123,7 +122,7 @@ export declare const mediaGenerationSettingsSchema: z.ZodObject<{
|
|
|
123
122
|
}, z.core.$strict>;
|
|
124
123
|
/** Caller settings before model defaults and refinements are applied. */
|
|
125
124
|
export type MediaGenerationSettingsInput = z.input<typeof mediaGenerationSettingsSchema>;
|
|
126
|
-
/**
|
|
125
|
+
/** A reference may select another Computer; identity never grants access. */
|
|
127
126
|
export declare const mediaGenerationSelectionSchema: z.ZodObject<{
|
|
128
127
|
model: z.ZodString;
|
|
129
128
|
prompt: z.ZodString;
|
|
@@ -149,6 +148,7 @@ export declare const mediaGenerationSelectionSchema: z.ZodObject<{
|
|
|
149
148
|
}>>;
|
|
150
149
|
generateAudio: z.ZodOptional<z.ZodBoolean>;
|
|
151
150
|
references: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
151
|
+
computerId: z.ZodOptional<z.ZodString>;
|
|
152
152
|
path: z.ZodString;
|
|
153
153
|
role: z.ZodOptional<z.ZodEnum<{
|
|
154
154
|
readonly first: "first-frame";
|
|
@@ -161,12 +161,14 @@ export declare const mediaGenerationSelectionSchema: z.ZodObject<{
|
|
|
161
161
|
export type MediaGenerationSelection = z.output<typeof mediaGenerationSelectionSchema>;
|
|
162
162
|
/** Validate caller settings and apply retained model defaults. */
|
|
163
163
|
export declare const validateMediaGenerationSettings: (raw: MediaGenerationSettingsInput) => MediaGenerationSettings;
|
|
164
|
-
/** Validate
|
|
164
|
+
/** Validate selections without granting Computer authority. */
|
|
165
165
|
export declare const validateMediaGenerationSelection: (raw: z.input<typeof mediaGenerationSelectionSchema>) => Readonly<Omit<MediaGenerationSettings, "references"> & {
|
|
166
166
|
/** Host-authorized reference images. */
|
|
167
167
|
references?: readonly Readonly<{
|
|
168
168
|
/** Fixed destination or reference file path. */
|
|
169
169
|
path: string;
|
|
170
|
+
/** Optional reference Computer; authorization remains host-owned. */
|
|
171
|
+
computerId?: string;
|
|
170
172
|
/** Video-only reference role. */
|
|
171
173
|
role?: MediaVideoReferenceRole;
|
|
172
174
|
}>[];
|
|
@@ -247,8 +249,8 @@ export type MediaVideoSelection = Readonly<{
|
|
|
247
249
|
export declare function validateMediaVideoSelection(raw: MediaVideoSelectionInput): MediaVideoSelection;
|
|
248
250
|
/** Host-authorized reader for decoded reference images. */
|
|
249
251
|
export type MediaReferenceReader = Readonly<{
|
|
250
|
-
/** Authorize and
|
|
251
|
-
read: (reference: MediaReference) => Promise<MediaReferenceBytes>;
|
|
252
|
+
/** Authorize and fit one reference for the selected catalog model. */
|
|
253
|
+
read: (reference: MediaReference, model: string) => Promise<MediaReferenceBytes>;
|
|
252
254
|
}>;
|
|
253
255
|
/** Resolve only provider-accepted references through the caller's reader. */
|
|
254
256
|
export declare const resolveMediaReferences: (settings: MediaGenerationSettings, reader: MediaReferenceReader) => Promise<readonly MediaReferenceBytes[]>;
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
2
|
import { reportCatchFailure } from "./failure.js";
|
|
3
|
+
import { referenceImageSizeIsValid } from "./reference-image-size.js";
|
|
4
|
+
export { MAX_REFERENCE_DIMENSION } from "./reference-image-size.js";
|
|
3
5
|
import { getMediaModel, MAX_REFERENCE_COUNT, MediaSettingsError, mediaVideoModelIdSchema, mediaVideoResolutionSchema, mediaVideoAspectRatioSchema, mediaVideoReferenceRoleSchema, MAX_MEDIA_PROMPT_LENGTH, } from "./model-catalog.js";
|
|
4
6
|
export { mediaFailureStageSchema } from "./execution-error.js";
|
|
5
7
|
export { MAX_MEDIA_PROMPT_LENGTH, getMediaModel, MAX_REFERENCE_COUNT, listMediaModels, mediaModelInputSchema, mediaModelSchema, mediaRequestedImageFormatSchema, mediaModels, mediaModelTierSchema, MediaSettingsError, mediaVideoModelIdSchema, mediaVideoResolutionSchema, mediaVideoAspectRatioSchema, mediaVideoReferenceRoleSchema, mediaVideoFormatSchema, mediaVideoSchema, } from "./model-catalog.js";
|
|
@@ -11,8 +13,6 @@ export const MAX_VIEW_DIMENSION = 512;
|
|
|
11
13
|
export const viewDimensionSchema = z.lazy(() => z.number().int().min(MIN_VIEW_DIMENSION).max(MAX_VIEW_DIMENSION));
|
|
12
14
|
/** Reference images in one video request: every verified video model's catalog value. */
|
|
13
15
|
const MAX_VIDEO_REFERENCE_COUNT = 4;
|
|
14
|
-
/** Maximum reference width and height sent to inference, in pixels. */
|
|
15
|
-
export const MAX_REFERENCE_DIMENSION = 512;
|
|
16
16
|
/** Maximum decoded bytes per reference image. */
|
|
17
17
|
export const MAX_REFERENCE_BYTES = 8 * 1024 * 1024;
|
|
18
18
|
/** Maximum retained still-image output bytes. */
|
|
@@ -74,12 +74,14 @@ export const mediaGenerationSettingsSchema = z
|
|
|
74
74
|
.optional(),
|
|
75
75
|
})
|
|
76
76
|
.strict();
|
|
77
|
-
/**
|
|
77
|
+
/** A reference may select another Computer; identity never grants access. */
|
|
78
78
|
export const mediaGenerationSelectionSchema = mediaGenerationSettingsSchema.extend({
|
|
79
79
|
/** Host-authorized reference images. */
|
|
80
80
|
references: z
|
|
81
81
|
.array(z
|
|
82
82
|
.object({
|
|
83
|
+
/** Omit for the selected Computer; the host authorizes every named Computer. */
|
|
84
|
+
computerId: z.string().min(1).max(256).optional(),
|
|
83
85
|
/** Fixed destination or reference file path. */
|
|
84
86
|
path: z.string().min(1).max(4_096),
|
|
85
87
|
/** Video-only reference role. */
|
|
@@ -96,15 +98,16 @@ export const validateMediaGenerationSettings = (raw) => {
|
|
|
96
98
|
throw new MediaSettingsError("generation settings are invalid");
|
|
97
99
|
return normalizeGenerationSettings(parsed.data, normalizeReference);
|
|
98
100
|
};
|
|
99
|
-
/** Validate
|
|
101
|
+
/** Validate selections without granting Computer authority. */
|
|
100
102
|
export const validateMediaGenerationSelection = (raw) => {
|
|
101
103
|
const parsed = mediaGenerationSelectionSchema.safeParse(raw);
|
|
102
104
|
if (!parsed.success)
|
|
103
105
|
throw new MediaSettingsError("generation settings are invalid");
|
|
104
106
|
return normalizeGenerationSettings(parsed.data, (reference) => {
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
107
|
+
const path = requiredString(reference.path, "reference path");
|
|
108
|
+
const selection = reference.computerId === undefined
|
|
109
|
+
? { path }
|
|
110
|
+
: { path, computerId: reference.computerId };
|
|
108
111
|
if (reference.role === undefined)
|
|
109
112
|
return selection;
|
|
110
113
|
return { ...selection, role: reference.role };
|
|
@@ -292,14 +295,13 @@ export const resolveMediaReferences = async (settings, reader) => {
|
|
|
292
295
|
throw new MediaSettingsError(`${model.id} accepts at most ${String(model.input.maxReferences)} reference images`);
|
|
293
296
|
const resolved = [];
|
|
294
297
|
for (const reference of references) {
|
|
295
|
-
const image = await reader.read(reference);
|
|
298
|
+
const image = await reader.read(reference, settings.model);
|
|
296
299
|
if (!imageFormats.has(image.format) ||
|
|
297
300
|
!Number.isSafeInteger(image.width) ||
|
|
298
301
|
!Number.isSafeInteger(image.height) ||
|
|
299
302
|
image.width < 1 ||
|
|
300
303
|
image.height < 1 ||
|
|
301
|
-
image.width
|
|
302
|
-
image.height > MAX_REFERENCE_DIMENSION ||
|
|
304
|
+
!referenceImageSizeIsValid(settings.model, image.width, image.height) ||
|
|
303
305
|
!(image.bytes instanceof Uint8Array) ||
|
|
304
306
|
image.bytes.byteLength === 0 ||
|
|
305
307
|
image.bytes.byteLength > MAX_REFERENCE_BYTES)
|
package/dist/media-binding.d.ts
CHANGED
|
@@ -26,6 +26,7 @@ export declare const mediaImageBindingInputSchema: z.ZodObject<{
|
|
|
26
26
|
width: z.ZodNumber;
|
|
27
27
|
height: z.ZodNumber;
|
|
28
28
|
references: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
29
|
+
computerId: z.ZodOptional<z.ZodString>;
|
|
29
30
|
path: z.ZodString;
|
|
30
31
|
role: z.ZodOptional<z.ZodEnum<{
|
|
31
32
|
readonly first: "first-frame";
|
|
@@ -112,6 +113,7 @@ export declare const mediaBindingGenerationSchema: z.ZodDiscriminatedUnion<[z.Zo
|
|
|
112
113
|
width: z.ZodNumber;
|
|
113
114
|
height: z.ZodNumber;
|
|
114
115
|
references: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
116
|
+
computerId: z.ZodOptional<z.ZodString>;
|
|
115
117
|
path: z.ZodString;
|
|
116
118
|
role: z.ZodOptional<z.ZodEnum<{
|
|
117
119
|
readonly first: "first-frame";
|
|
@@ -173,7 +175,7 @@ export declare const mediaBindingGenerationSchema: z.ZodDiscriminatedUnion<[z.Zo
|
|
|
173
175
|
}, z.core.$strict>], "kind">;
|
|
174
176
|
/** Parsed generation command consumed by either host. */
|
|
175
177
|
export type MediaBindingGeneration = z.output<typeof mediaBindingGenerationSchema>;
|
|
176
|
-
/** Maintained MEDIA
|
|
178
|
+
/** Maintained MEDIA grammar; reference selectors carry no Computer authority. */
|
|
177
179
|
export declare const mediaBindingCallSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
178
180
|
operation: z.ZodLiteral<"models">;
|
|
179
181
|
input: z.ZodObject<{}, z.core.$strict>;
|
|
@@ -203,6 +205,7 @@ export declare const mediaBindingCallSchema: z.ZodDiscriminatedUnion<[z.ZodObjec
|
|
|
203
205
|
width: z.ZodNumber;
|
|
204
206
|
height: z.ZodNumber;
|
|
205
207
|
references: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
208
|
+
computerId: z.ZodOptional<z.ZodString>;
|
|
206
209
|
path: z.ZodString;
|
|
207
210
|
role: z.ZodOptional<z.ZodEnum<{
|
|
208
211
|
readonly first: "first-frame";
|
|
@@ -286,7 +289,7 @@ export declare function parseMediaBindingCall(raw: z.core.util.JSONType): Readon
|
|
|
286
289
|
export type MediaBindingRequest = Readonly<{
|
|
287
290
|
/** Host-selected output filename within the App's library folder. */
|
|
288
291
|
path: string;
|
|
289
|
-
/** Validated
|
|
292
|
+
/** Validated references with explicit Computer identities, authorized by the host. */
|
|
290
293
|
settings: MediaGenerationSettings;
|
|
291
294
|
/** Optional image encoding; video output remains MP4. */
|
|
292
295
|
outputFormat?: z.output<typeof mediaRequestedImageFormatSchema>;
|
package/dist/media-binding.js
CHANGED
|
@@ -51,7 +51,7 @@ export const mediaBindingGenerationSchema = z.discriminatedUnion("kind", [
|
|
|
51
51
|
mediaImageBindingInputSchema,
|
|
52
52
|
mediaVideoBindingInputSchema,
|
|
53
53
|
]);
|
|
54
|
-
/** Maintained MEDIA
|
|
54
|
+
/** Maintained MEDIA grammar; reference selectors carry no Computer authority. */
|
|
55
55
|
export const mediaBindingCallSchema = z.discriminatedUnion("operation", [
|
|
56
56
|
z.strictObject({
|
|
57
57
|
/** Read the model catalog without dispatching generation. */
|
|
@@ -120,7 +120,12 @@ export function mediaBindingRequest(input, computerId) {
|
|
|
120
120
|
if (references !== undefined)
|
|
121
121
|
attached = {
|
|
122
122
|
...settings,
|
|
123
|
-
references: references.map((reference) => ({
|
|
123
|
+
references: references.map((reference) => ({
|
|
124
|
+
...reference,
|
|
125
|
+
computerId: "computerId" in reference
|
|
126
|
+
? (reference.computerId ?? computerId)
|
|
127
|
+
: computerId,
|
|
128
|
+
})),
|
|
124
129
|
};
|
|
125
130
|
const request = {
|
|
126
131
|
path: `${input.libraryPath}/prints-${input.requestId}.${format}`,
|
package/dist/media-generation.js
CHANGED
|
@@ -47,10 +47,10 @@ export function runMediaGeneration(host, request, signal) {
|
|
|
47
47
|
generation: {
|
|
48
48
|
prepare: async () => {
|
|
49
49
|
const references = await resolveMediaReferences(settings, {
|
|
50
|
-
read: (reference) => {
|
|
50
|
+
read: (reference, model) => {
|
|
51
51
|
if (host.destination.readReference === undefined)
|
|
52
52
|
throw new MediaSettingsError("reference images are unavailable");
|
|
53
|
-
return host.destination.readReference(reference);
|
|
53
|
+
return host.destination.readReference(reference, model);
|
|
54
54
|
},
|
|
55
55
|
});
|
|
56
56
|
const costMilliunits = estimate(settings, references);
|
package/dist/media-operation.js
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
import { MediaExecutionError, MediaOutputFormatMismatch, MediaSafetyFilterRefusal, } from "./execution-error.js";
|
|
2
2
|
import * as Effect from "effect/Effect";
|
|
3
|
+
import { reportCatchFailure } from "./failure.js";
|
|
3
4
|
import { mediaUnits } from "./gateway-models.js";
|
|
4
5
|
import { getMediaModel } from "./model-catalog.js";
|
|
5
6
|
import { embedProvenance } from "./provenance.js";
|
|
6
7
|
import { z } from "zod";
|
|
7
8
|
import { mediaGenerationSettingsSchema, validateMediaGenerationSettings, } from "./index.js";
|
|
9
|
+
function unchargedFailure(error) {
|
|
10
|
+
const options = {
|
|
11
|
+
cause: error.cause,
|
|
12
|
+
provider: error.provider,
|
|
13
|
+
customerCharge: "none",
|
|
14
|
+
};
|
|
15
|
+
return error instanceof MediaSafetyFilterRefusal
|
|
16
|
+
? new MediaSafetyFilterRefusal(options)
|
|
17
|
+
: new MediaExecutionError(error.stage, options);
|
|
18
|
+
}
|
|
8
19
|
function call(body, stage) {
|
|
9
20
|
return Effect.tryPromise({
|
|
10
21
|
try: () => Promise.resolve(body()),
|
|
@@ -49,10 +60,10 @@ export function executeMediaOperation(input) {
|
|
|
49
60
|
const operationId = admission.operation.operationId;
|
|
50
61
|
const billed = mediaUnits(validateMediaGenerationSettings(mediaGenerationSettingsSchema.strip().parse(input.request)));
|
|
51
62
|
const background = getMediaModel(input.request.model)?.kind === "video";
|
|
52
|
-
/**
|
|
63
|
+
/** Best-effort release; true proves customer settlement, not supplier billing. */
|
|
53
64
|
const settleNothing = () => metering === null
|
|
54
|
-
? Effect.
|
|
55
|
-
: call(() => metering.settle({ operationId, units: 0 }), "billing").pipe(Effect.
|
|
65
|
+
? Effect.succeed(false)
|
|
66
|
+
: call(() => metering.settle({ operationId, units: 0 }), "billing").pipe(Effect.match({ onFailure: () => false, onSuccess: () => true }));
|
|
56
67
|
const claim = yield* call(() => input.operation.claim(), "operation");
|
|
57
68
|
if (claim.kind === "succeeded" &&
|
|
58
69
|
claim.operation.image !== null &&
|
|
@@ -71,14 +82,16 @@ export function executeMediaOperation(input) {
|
|
|
71
82
|
}));
|
|
72
83
|
}
|
|
73
84
|
if (claim.kind === "interrupted") {
|
|
74
|
-
yield* settleNothing();
|
|
85
|
+
const uncharged = yield* settleNothing();
|
|
75
86
|
const failure = claim.operation.providerFailure;
|
|
76
|
-
if (failure !== null)
|
|
77
|
-
|
|
87
|
+
if (failure !== null) {
|
|
88
|
+
const error = failure.safetyRefused
|
|
78
89
|
? new MediaSafetyFilterRefusal({ provider: failure.provider })
|
|
79
90
|
: new MediaExecutionError(failure.stage, {
|
|
80
91
|
provider: failure.provider,
|
|
81
|
-
})
|
|
92
|
+
});
|
|
93
|
+
return yield* Effect.fail(uncharged ? unchargedFailure(error) : error);
|
|
94
|
+
}
|
|
82
95
|
}
|
|
83
96
|
if (claim.kind !== "claimed" || claim.leaseId === undefined)
|
|
84
97
|
return {
|
|
@@ -154,16 +167,22 @@ export function executeMediaOperation(input) {
|
|
|
154
167
|
}, "provider").pipe(
|
|
155
168
|
// A provider failure delivered nothing and wrote no output: end the
|
|
156
169
|
// request now and release its hold rather than leave it unresolved.
|
|
157
|
-
Effect.
|
|
170
|
+
Effect.catch((error) => Effect.gen(function* () {
|
|
158
171
|
const stage = error.stage;
|
|
159
|
-
|
|
160
|
-
|
|
172
|
+
if (stage === "provider" || stage === "normalization") {
|
|
173
|
+
const uncharged = yield* call(() => input.operation.interruptGeneration(leaseId, {
|
|
161
174
|
stage,
|
|
162
175
|
safetyRefused: error instanceof MediaSafetyFilterRefusal,
|
|
163
176
|
provider: error.provider,
|
|
164
|
-
}), "operation").pipe(Effect.andThen(settleNothing()), Effect.
|
|
165
|
-
|
|
166
|
-
|
|
177
|
+
}), "operation").pipe(Effect.andThen(settleNothing()), Effect.catch((cause) => {
|
|
178
|
+
reportCatchFailure("media-operation-interrupt", cause);
|
|
179
|
+
return Effect.succeed(false);
|
|
180
|
+
}));
|
|
181
|
+
if (uncharged)
|
|
182
|
+
return yield* Effect.fail(unchargedFailure(error));
|
|
183
|
+
}
|
|
184
|
+
return yield* Effect.fail(error);
|
|
185
|
+
})));
|
|
167
186
|
if (generation.kind === "interrupted") {
|
|
168
187
|
yield* settleNothing();
|
|
169
188
|
return { kind: "interrupted" };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Maximum long edge for image-model references, in pixels. */
|
|
2
|
+
export declare const MAX_REFERENCE_DIMENSION = 512;
|
|
3
|
+
/** Aspect-preserving native resize; only video references may be enlarged. */
|
|
4
|
+
export declare function referenceImageTransform(modelId: string, width: number, height: number): {
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
fit: "contain" | "scale-down";
|
|
8
|
+
};
|
|
9
|
+
/** Validate actual decoded output, rather than trusting requested resize dimensions. */
|
|
10
|
+
export declare function referenceImageSizeIsValid(modelId: string, width: number, height: number): boolean;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { getMediaModel, MediaSettingsError } from "./model-catalog.js";
|
|
2
|
+
/** Maximum long edge for image-model references, in pixels. */
|
|
3
|
+
export const MAX_REFERENCE_DIMENSION = 512;
|
|
4
|
+
const MIN_VIDEO_REFERENCE_DIMENSION = 320;
|
|
5
|
+
/** One owner for both native fitting and the resolved-reference boundary. */
|
|
6
|
+
function sizing(modelId) {
|
|
7
|
+
const model = getMediaModel(modelId);
|
|
8
|
+
if (model === undefined)
|
|
9
|
+
throw new MediaSettingsError("unknown Media model");
|
|
10
|
+
return model.video === null
|
|
11
|
+
? { edge: "long", dimension: MAX_REFERENCE_DIMENSION }
|
|
12
|
+
: { edge: "short", dimension: MIN_VIDEO_REFERENCE_DIMENSION };
|
|
13
|
+
}
|
|
14
|
+
/** Aspect-preserving native resize; only video references may be enlarged. */
|
|
15
|
+
export function referenceImageTransform(modelId, width, height) {
|
|
16
|
+
const rule = sizing(modelId);
|
|
17
|
+
const edge = rule.edge === "short" ? Math.min(width, height) : Math.max(width, height);
|
|
18
|
+
const ratio = rule.edge === "short"
|
|
19
|
+
? rule.dimension / edge
|
|
20
|
+
: Math.min(1, rule.dimension / edge);
|
|
21
|
+
// A video's bounding box must not round its long edge down and squeeze
|
|
22
|
+
// the actual decoded short edge below the provider minimum.
|
|
23
|
+
const round = rule.edge === "short" ? Math.ceil : Math.round;
|
|
24
|
+
return {
|
|
25
|
+
width: Math.max(1, round(width * ratio)),
|
|
26
|
+
height: Math.max(1, round(height * ratio)),
|
|
27
|
+
// Small video frames must grow; image references must never grow.
|
|
28
|
+
fit: rule.edge === "short" ? "contain" : "scale-down",
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** Validate actual decoded output, rather than trusting requested resize dimensions. */
|
|
32
|
+
export function referenceImageSizeIsValid(modelId, width, height) {
|
|
33
|
+
const rule = sizing(modelId);
|
|
34
|
+
return rule.edge === "short"
|
|
35
|
+
? Math.min(width, height) >= rule.dimension
|
|
36
|
+
: Math.max(width, height) <= rule.dimension;
|
|
37
|
+
}
|
package/dist/server.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { estimateMediaModel } from "./cloudflare/provider.js";
|
|
1
2
|
export { mediaPendingSchema, mediaBindingCallSchema, mediaBindingGenerationSchema, mediaImageBindingInputSchema, mediaVideoBindingInputSchema, parseMediaBindingCall, mediaBindingRequest, readMediaBindingReply, type MediaBindingCall, type MediaBindingGeneration, type MediaBindingRequest, } from "./media-binding.js";
|
|
2
3
|
import { Hono } from "hono";
|
|
3
4
|
import { z } from "zod";
|
|
@@ -90,8 +91,8 @@ export type MediaServiceHost = Readonly<{
|
|
|
90
91
|
authorize: (path: string) => Promise<void>;
|
|
91
92
|
/** Save exact generated bytes to the admitted destination. */
|
|
92
93
|
write: (path: string, bytes: Uint8Array) => Promise<void>;
|
|
93
|
-
/** Authorize and
|
|
94
|
-
readReference?: (reference: MediaReference) => Promise<MediaReferenceBytes>;
|
|
94
|
+
/** Authorize and fit one selected reference image for the catalog model. */
|
|
95
|
+
readReference?: (reference: MediaReference, model: string) => Promise<MediaReferenceBytes>;
|
|
95
96
|
}>;
|
|
96
97
|
/** Translate host authority failures; undefined uses the shared Media failure. */
|
|
97
98
|
onFailure?: (error: MediaExecutionError) => Response | undefined;
|