@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.
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 +98 -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 +611 -0
  29. package/dist/media-binding.d.ts +291 -0
  30. package/dist/media-binding.js +146 -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/server.js ADDED
@@ -0,0 +1,207 @@
1
+ import { mediaPendingSchema } from "./media-binding.js";
2
+ export { mediaPendingSchema, mediaBindingCallSchema, mediaBindingGenerationSchema, mediaImageBindingInputSchema, mediaVideoBindingInputSchema, parseMediaBindingCall, mediaBindingRequest, readMediaBindingReply, } from "./media-binding.js";
3
+ import { Hono } from "hono";
4
+ import { z } from "zod";
5
+ import * as Effect from "effect/Effect";
6
+ import * as Result from "effect/Result";
7
+ import { runMediaGeneration } from "./media-generation.js";
8
+ import { validateMediaGenerationSettings, listMediaModels, mediaGenerationSettingsSchema, mediaRequestedImageFormatSchema, MediaSettingsError, } from "./index.js";
9
+ import { mediaResultSchema, } from "./media-operation-contract.js";
10
+ import { MediaOutputFormatMismatch, MediaBillingRefusal, MediaExecutionError, MediaSafetyFilterRefusal, } from "./execution-error.js";
11
+ export { MediaBillingRefusal, MediaExecutionError, } from "./execution-error.js";
12
+ export { mediaUnits } from "./gateway-models.js";
13
+ export { MediaOperationOwner, mediaOperationDestinationSchema, mediaOperationCoordinatesSchema, mediaOperationStateSchema, mediaImageSchema, mediaOperationResultSchema, } from "./media-operation-owner.js";
14
+ const identifier = z.string().min(1).max(256);
15
+ /** Decode relative service generation input. Identity comes from the host. */
16
+ export const mediaRequestSchema = z.strictObject({
17
+ /** Fixed destination or reference file path. */
18
+ path: z
19
+ .string()
20
+ .min(1)
21
+ .max(1_024)
22
+ .refine((path) => !path.includes("\0")),
23
+ /** Requested model, dimensions, prompt and references. */
24
+ settings: mediaGenerationSettingsSchema,
25
+ /** Optional verified image encoding pin. */
26
+ outputFormat: mediaRequestedImageFormatSchema.optional(),
27
+ });
28
+ export { mediaResultSchema } from "./media-operation-contract.js";
29
+ async function readJsonBody(request) {
30
+ try {
31
+ const value = await request.json();
32
+ return {
33
+ kind: "parsed",
34
+ value,
35
+ };
36
+ }
37
+ catch {
38
+ return {
39
+ kind: "invalid",
40
+ };
41
+ }
42
+ }
43
+ const headers = {
44
+ "Cache-Control": "private, no-store",
45
+ "X-Content-Type-Options": "nosniff",
46
+ };
47
+ function failure(error, refusalMessages) {
48
+ if (error instanceof MediaSafetyFilterRefusal)
49
+ return Response.json({
50
+ error: "The model's safety filter refused this prompt — try rewording.",
51
+ stage: "provider",
52
+ code: "SAFETY_FILTER_REFUSED",
53
+ retryable: false,
54
+ }, { status: 400, headers });
55
+ if (error instanceof MediaExecutionError &&
56
+ error.cause instanceof MediaOutputFormatMismatch)
57
+ return Response.json({
58
+ error: "Media provider did not return the requested image format; nothing was saved or charged. Start a new request.",
59
+ stage: error.stage,
60
+ code: "HOST_FAILURE",
61
+ retryable: false,
62
+ }, {
63
+ status: 503,
64
+ headers,
65
+ });
66
+ if (error instanceof MediaExecutionError &&
67
+ error.stage === "billing" &&
68
+ error.cause instanceof MediaBillingRefusal)
69
+ return Response.json({
70
+ error: refusalMessages?.[error.cause.code] ??
71
+ "Media generation was refused by the host.",
72
+ stage: "billing",
73
+ code: error.cause.code === "insufficient_credits"
74
+ ? "INSUFFICIENT_CREDIT"
75
+ : "CAPABILITY_DENIED",
76
+ retryable: false,
77
+ }, {
78
+ status: error.cause.code === "denied" ? 403 : 402,
79
+ headers,
80
+ });
81
+ if (error instanceof MediaSettingsError ||
82
+ (error.stage === "estimate" && error.cause instanceof MediaSettingsError))
83
+ return Response.json({
84
+ error: "Media generation settings are invalid; check the model, dimensions, steps, and destination.",
85
+ stage: "settings",
86
+ code: "INVALID_ARGUMENT",
87
+ retryable: false,
88
+ }, {
89
+ status: 400,
90
+ headers,
91
+ });
92
+ const messages = {
93
+ "generation-authorization": "Media generation permission could not be checked; try again.",
94
+ "destination-authorization": "Media destination permission could not be checked; try again.",
95
+ "destination-write": "Media generated an image but could not save it; retry this request to save the retained result.",
96
+ estimate: "Media could not prepare the image inputs or estimate their cost; check the reference files and retry.",
97
+ billing: "Media billing is unavailable; retry this request.",
98
+ provider: "Media provider failed; nothing was charged. Start a new request.",
99
+ normalization: "Media provider returned an unusable image; start a new request only after checking provider status.",
100
+ artifact: "Media generated output could not be retained or read; try again.",
101
+ operation: "Media operation is unresolved; try again.",
102
+ };
103
+ return Response.json({
104
+ error: messages[error.stage],
105
+ stage: error.stage,
106
+ code: "HOST_FAILURE",
107
+ retryable: error.stage !== "normalization" && error.stage !== "provider",
108
+ }, {
109
+ status: 503,
110
+ headers,
111
+ });
112
+ }
113
+ /** Generate through the Effect core with a Promise boundary preserving typed domain failures. */
114
+ export async function generateMedia(host, request, signal) {
115
+ const result = await Effect.runPromise(Effect.result(runMediaGeneration(host, request, signal)));
116
+ return Result.getOrThrowWith(result, (error) => error);
117
+ }
118
+ function outcomeResponse(outcome) {
119
+ if (outcome.kind === "conflict")
120
+ return Response.json({
121
+ error: "This request ID already belongs to a different Media request.",
122
+ stage: "operation",
123
+ code: "INVALID_ARGUMENT",
124
+ retryable: false,
125
+ }, {
126
+ status: 409,
127
+ headers,
128
+ });
129
+ if (outcome.kind === "pending" && outcome.background)
130
+ // A video generation runs in the background; retry this request id.
131
+ return Response.json(mediaPendingSchema.parse({
132
+ status: "pending",
133
+ retryAfterSeconds: outcome.retryAfterSeconds,
134
+ }), {
135
+ status: 202,
136
+ headers: {
137
+ ...headers,
138
+ "Retry-After": String(outcome.retryAfterSeconds),
139
+ },
140
+ });
141
+ if (outcome.kind === "pending")
142
+ return Response.json({
143
+ error: "Media generation is still in progress; retry this request to check its durable result.",
144
+ stage: "operation",
145
+ code: "HOST_FAILURE",
146
+ retryable: true,
147
+ }, {
148
+ status: 409,
149
+ headers,
150
+ });
151
+ if (outcome.kind === "interrupted" || outcome.kind === "unavailable")
152
+ return Response.json({
153
+ error: "Media generation was interrupted before completion; start a new request.",
154
+ stage: "operation",
155
+ code: "HOST_FAILURE",
156
+ retryable: false,
157
+ }, {
158
+ status: 503,
159
+ headers,
160
+ });
161
+ return Response.json(mediaResultSchema.parse({
162
+ bytes: outcome.bytes,
163
+ costMilliunits: outcome.costMilliunits,
164
+ path: outcome.path,
165
+ format: outcome.format,
166
+ height: outcome.height,
167
+ model: outcome.model,
168
+ width: outcome.width,
169
+ }), { headers });
170
+ }
171
+ /** Relative HTTP routes; the caller admits identity and supplies destination access. */
172
+ export function createMediaService(host) {
173
+ const app = new Hono();
174
+ app.get("/models", () => Response.json(listMediaModels().map((model) => host.destination.readReference !== undefined ||
175
+ model.input.maxReferences === 0
176
+ ? model
177
+ : {
178
+ ...model,
179
+ label: `${model.label} (reference images unavailable in this App host)`,
180
+ input: {
181
+ ...model.input,
182
+ maxReferences: 0,
183
+ referenceField: null,
184
+ },
185
+ }), { headers }));
186
+ app.post("/generate", async (context) => {
187
+ const body = await readJsonBody(context.req.raw);
188
+ const input = body.kind === "parsed"
189
+ ? mediaRequestSchema.safeParse(body.value)
190
+ : undefined;
191
+ const requestId = identifier.safeParse(context.req.header("X-Request-Id"));
192
+ if (input?.success !== true || !requestId.success)
193
+ return failure(new MediaSettingsError("invalid request"));
194
+ return Effect.runPromise(runMediaGeneration(host, {
195
+ requestId: requestId.data,
196
+ path: input.data.path,
197
+ settings: input.data.settings,
198
+ outputFormat: input.data.outputFormat,
199
+ }, context.req.raw.signal).pipe(Effect.match({
200
+ onSuccess: outcomeResponse,
201
+ onFailure: (error) => (error instanceof MediaExecutionError
202
+ ? host.onFailure?.(error)
203
+ : undefined) ?? failure(error, host.refusalMessages),
204
+ })));
205
+ });
206
+ return app;
207
+ }
@@ -0,0 +1,14 @@
1
+ import type { VideoProviderAdapter } from "./types.js";
2
+ export type { CheckedVideoSettings } from "./types.js";
3
+ /**
4
+ * One adapter per verified video provider family. Seedance and Veo share
5
+ * `unifiedVideoAdapter`; MiniMax's distinct `content[]` request and
6
+ * `{task: {...}}` answer get their own module. Adding a provider means
7
+ * writing one adapter module and one entry here — `gatewayInput` and
8
+ * `generateThroughGateway` never branch on the provider name themselves.
9
+ */
10
+ export declare const videoProviderAdapters: {
11
+ seedance: VideoProviderAdapter;
12
+ veo: VideoProviderAdapter;
13
+ minimax: VideoProviderAdapter;
14
+ };
@@ -0,0 +1,14 @@
1
+ import { unifiedVideoAdapter } from "./unified.js";
2
+ import { minimaxVideoAdapter } from "./minimax.js";
3
+ /**
4
+ * One adapter per verified video provider family. Seedance and Veo share
5
+ * `unifiedVideoAdapter`; MiniMax's distinct `content[]` request and
6
+ * `{task: {...}}` answer get their own module. Adding a provider means
7
+ * writing one adapter module and one entry here — `gatewayInput` and
8
+ * `generateThroughGateway` never branch on the provider name themselves.
9
+ */
10
+ export const videoProviderAdapters = {
11
+ seedance: unifiedVideoAdapter,
12
+ veo: unifiedVideoAdapter,
13
+ minimax: minimaxVideoAdapter,
14
+ };
@@ -0,0 +1,25 @@
1
+ import type { VideoProviderAdapter } from "./types.js";
2
+ /** MiniMax's documented `content[]` image role, distinct from our neutral one. */
3
+ export type MinimaxReferenceRole = "first_frame" | "last_frame" | "reference_image";
4
+ /** One MiniMax `content[]` entry, exactly as its schema documents it. */
5
+ export type MinimaxContentItem = {
6
+ type: "text";
7
+ text: string;
8
+ } | {
9
+ type: "image_url";
10
+ image_url: {
11
+ url: string;
12
+ };
13
+ role: MinimaxReferenceRole;
14
+ };
15
+ /** MiniMax's documented uppercase resolution literal. */
16
+ export type MinimaxResolution = "480P" | "720P" | "768P" | "1080P" | "4K";
17
+ /** MiniMax H3 Max's documented provider body: multimodal `content[]`, not a
18
+ * flat prompt/image/duration object. */
19
+ export type MinimaxGatewayInput = {
20
+ content: MinimaxContentItem[];
21
+ duration: number;
22
+ resolution: MinimaxResolution;
23
+ ratio: string;
24
+ };
25
+ export declare const minimaxVideoAdapter: VideoProviderAdapter;
@@ -0,0 +1,63 @@
1
+ import * as z from "zod";
2
+ import { MediaSettingsError } from "../model-catalog.js";
3
+ import { resultUrl } from "../gateway-wire.js";
4
+ import { imageDataUri } from "./reference-encoding.js";
5
+ /** Our lowercase selector to MiniMax's documented uppercase wire literal. */
6
+ const minimaxResolutionWire = {
7
+ "480p": "480P",
8
+ "720p": "720P",
9
+ "768p": "768P",
10
+ "1080p": "1080P",
11
+ "4k": "4K",
12
+ };
13
+ /** Our neutral reference role to MiniMax's documented `content[]` role. */
14
+ const minimaxRoleWire = {
15
+ "first-frame": "first_frame",
16
+ "last-frame": "last_frame",
17
+ reference: "reference_image",
18
+ };
19
+ /** MiniMax's documented task wrapper: `{task: {status, content: {url}}}`,
20
+ * distinct from the Unified gateway's `{state, result}` shape Seedance and
21
+ * Veo normalize into. https://developers.cloudflare.com/ai/models/minimax/h3-max/ */
22
+ const minimaxTaskOutputSchema = z.looseObject({
23
+ task: z.looseObject({
24
+ status: z.enum(["queued", "running", "succeeded", "failed", "cancelled"]),
25
+ content: z.looseObject({ url: resultUrl.optional() }).optional(),
26
+ }),
27
+ });
28
+ export const minimaxVideoAdapter = {
29
+ buildInput(checked, references, model) {
30
+ if (checked.generateAudio !== undefined)
31
+ throw new MediaSettingsError(`${model.id} does not support an audio choice`);
32
+ const content = [
33
+ { type: "text", text: checked.prompt },
34
+ ];
35
+ references.forEach((image, index) => {
36
+ const role = checked.references?.[index]?.role;
37
+ const wireRole = role === undefined ? undefined : minimaxRoleWire[role];
38
+ if (wireRole === undefined)
39
+ throw new MediaSettingsError("video reference role was not admitted");
40
+ content.push({
41
+ type: "image_url",
42
+ image_url: { url: imageDataUri(image) },
43
+ role: wireRole,
44
+ });
45
+ });
46
+ const input = {
47
+ content,
48
+ duration: checked.durationSeconds,
49
+ resolution: minimaxResolutionWire[checked.resolution],
50
+ ratio: checked.aspectRatio,
51
+ };
52
+ return input;
53
+ },
54
+ decodeResultUrl(providerValue) {
55
+ const parsed = minimaxTaskOutputSchema.parse(providerValue);
56
+ if (parsed.task.status !== "succeeded")
57
+ throw new Error(`minimax video task ${parsed.task.status}`);
58
+ const url = parsed.task.content?.url;
59
+ if (url === undefined)
60
+ throw new Error("minimax video task has no URL");
61
+ return url;
62
+ },
63
+ };
@@ -0,0 +1,4 @@
1
+ import type { MediaReferenceBytes } from "../index.js";
2
+ /** Encode in multiples of three so only the final chunk needs base64 padding. */
3
+ export declare function imageBase64(image: MediaReferenceBytes): string;
4
+ export declare function imageDataUri(image: MediaReferenceBytes): string;
@@ -0,0 +1,11 @@
1
+ /** Encode in multiples of three so only the final chunk needs base64 padding. */
2
+ export function imageBase64(image) {
3
+ const chunks = [];
4
+ for (let offset = 0; offset < image.bytes.length; offset += 32_766)
5
+ chunks.push(btoa(String.fromCharCode(...image.bytes.subarray(offset, offset + 32_766))));
6
+ return chunks.join("");
7
+ }
8
+ export function imageDataUri(image) {
9
+ const mime = image.format === "jpeg" ? "image/jpeg" : `image/${image.format}`;
10
+ return `data:${mime};base64,${imageBase64(image)}`;
11
+ }
@@ -0,0 +1,28 @@
1
+ import type { MediaAiResult, MediaGenerationSettings, MediaReferenceBytes } from "../index.js";
2
+ import type { MediaModel, MediaVideoAspectRatio, MediaVideoModel, MediaVideoResolution } from "../model-catalog.js";
3
+ import type { UnifiedGatewayInput } from "./unified.js";
4
+ import type { MinimaxGatewayInput } from "./minimax.js";
5
+ /** One verified video provider family, as the catalog names it. */
6
+ export type VideoProvider = MediaVideoModel["provider"];
7
+ /** Validated video settings with every video-only field guaranteed present;
8
+ * `gatewayInput` narrows to this before handing settings to an adapter. */
9
+ export type CheckedVideoSettings = MediaGenerationSettings & {
10
+ resolution: MediaVideoResolution;
11
+ aspectRatio: MediaVideoAspectRatio;
12
+ durationSeconds: number;
13
+ };
14
+ /** Every documented provider request body this catalog can produce. */
15
+ export type VideoProviderInput = UnifiedGatewayInput | MinimaxGatewayInput;
16
+ /**
17
+ * One provider family's wire translation: the exact request body its
18
+ * documented schema expects, and how to pull a validated result URL out of
19
+ * its answer. Adding a provider means writing one adapter and one table
20
+ * entry; `gatewayInput` and `generateThroughGateway` never branch on the
21
+ * provider name themselves.
22
+ */
23
+ export type VideoProviderAdapter = {
24
+ /** Build the provider JSON body for one validated video generation. */
25
+ buildInput(checked: CheckedVideoSettings, references: readonly MediaReferenceBytes[], model: MediaModel): VideoProviderInput;
26
+ /** Extract and validate the HTTPS result URL from the raw provider answer. */
27
+ decodeResultUrl(providerValue: MediaAiResult): string;
28
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,18 @@
1
+ import type { VideoProviderAdapter } from "./types.js";
2
+ export type UnifiedGatewayInput = {
3
+ prompt: string;
4
+ duration: string | number;
5
+ resolution: string;
6
+ aspect_ratio: string;
7
+ generate_audio?: boolean;
8
+ image?: string;
9
+ last_frame_image?: string;
10
+ reference_images?: string[];
11
+ image_input?: string;
12
+ };
13
+ /**
14
+ * Seedance and Veo share the Unified AI gateway's `{state, result}` answer
15
+ * and a flat prompt/image request body; only a few fields differ by
16
+ * provider, handled inline below.
17
+ */
18
+ export declare const unifiedVideoAdapter: VideoProviderAdapter;
@@ -0,0 +1,52 @@
1
+ import { MediaSettingsError } from "../model-catalog.js";
2
+ import { gatewayOutputSchema } from "../gateway-wire.js";
3
+ import { imageBase64, imageDataUri } from "./reference-encoding.js";
4
+ /**
5
+ * Seedance and Veo share the Unified AI gateway's `{state, result}` answer
6
+ * and a flat prompt/image request body; only a few fields differ by
7
+ * provider, handled inline below.
8
+ */
9
+ export const unifiedVideoAdapter = {
10
+ buildInput(checked, references, model) {
11
+ const veo = model.video?.provider === "veo";
12
+ const input = {
13
+ prompt: checked.prompt,
14
+ duration: veo
15
+ ? `${String(checked.durationSeconds)}s`
16
+ : checked.durationSeconds,
17
+ resolution: checked.resolution,
18
+ aspect_ratio: checked.aspectRatio,
19
+ };
20
+ if (checked.generateAudio !== undefined)
21
+ input.generate_audio = checked.generateAudio;
22
+ const referenceImages = [];
23
+ references.forEach((image, index) => {
24
+ const role = checked.references?.[index]?.role;
25
+ if (veo) {
26
+ input.image_input = imageBase64(image);
27
+ }
28
+ else if (role === "first-frame") {
29
+ input.image = imageDataUri(image);
30
+ }
31
+ else if (role === "last-frame") {
32
+ input.last_frame_image = imageDataUri(image);
33
+ }
34
+ else if (role === "reference") {
35
+ referenceImages.push(imageDataUri(image));
36
+ }
37
+ else
38
+ throw new MediaSettingsError("video reference role was not admitted");
39
+ });
40
+ if (referenceImages.length > 0)
41
+ input.reference_images = referenceImages;
42
+ return input;
43
+ },
44
+ decodeResultUrl(providerValue) {
45
+ const parsed = gatewayOutputSchema.parse(providerValue);
46
+ if (parsed.state !== "Completed")
47
+ throw new Error("gateway video result is not completed");
48
+ if (parsed.result.video === undefined)
49
+ throw new Error("gateway video result has no URL");
50
+ return parsed.result.video;
51
+ },
52
+ };
@@ -0,0 +1,16 @@
1
+ <!-- Generated by Botanical API docs (TypeDoc + typedoc-plugin-markdown). Do not edit. -->
2
+
3
+ **@fungi.computer/darkroom**
4
+
5
+ ---
6
+
7
+ # Darkroom
8
+
9
+ Generate media through a host-supplied provider, retain its exact output, and
10
+ retry saving without repeating inference. Your host owns identity, permission,
11
+ billing and destination access.
12
+
13
+ Use the root entry for the model catalog and settings. `server` composes the
14
+ service and portable SQL owner. `cloudflare` binds that owner to a SQLite
15
+ Durable Object and supplies Workers AI and Images adapters. `provenance` strips
16
+ private image metadata for downloads.