Setting the file. One moment.
Images · Wix Headless Fast · wix/skills · Skills Docs
ContentsBack to the top of the page references/shared/seed/ images.mjs
JavaScript · 145 lines · 7 KB
14 import { randomUUID } from "node:crypto" ;
15 import { readFileSync } from "node:fs" ;
16 import { basename, extname } from "node:path" ;
17
18 const API = "https://www.wixapis.com" ;
19 // Order = cheap-and-permissive first (runware ~0.009 credits/img, ~5s, loosest content
20 // filter), then google (best fidelity, ~0.14, ~25s; rejects steps/CFGScale and free-form
21 // sizes), then bfl (strictest filter — refuses trademark-ish prompts). A refusal or failure
22 // falls through to the next model.
23 const MODELS = [ "runware:400@1" , "google:4@2" , "bfl:5@1" ];
24 /** Allowed dimensions: 1024×1024 (square — entities), 1376×768 (16:9 hero), 1200×896 (4:3). */
25 export const IMAGE_SIZES = { square: [ 1024 , 1024 ], hero: [ 1376 , 768 ], editorial: [ 1200 , 896 ] };
26
27 async function req ( ctx , path , body , timeoutMs = 45_000 ) {
28 const res = await fetch ( API + path, {
29 method: "POST" ,
30 headers: {
31 Authorization: `Bearer ${ ctx . token }` ,
32 "wix-site-id" : ctx.siteId,
33 "Content-Type" : "application/json" ,
34 },
35 body: JSON . stringify (body),
36 signal: AbortSignal. timeout (timeoutMs),
37 });
38 const json = await res. json (). catch (() => ({}));
39 if ( ! res.ok) throw new Error ( `POST ${ path } -> ${ res . status }: ${ JSON . stringify ( json ). slice ( 0 , 300 ) }` );
40 return json;
41 }
42
43 /**
44 * Generate one image; returns its short-lived URL (import it immediately). Tries each model
45 * once — a per-model failure (bad params, 5xx, credit exhaustion, timeout) falls through to
46 * the next; throws only after all models failed.
47 * docs: no public reference for /runwareschemaless/v1/request — see wix-headless/references/IMAGE_GENERATION.md
48 */
49 export async function generateImage ( ctx , prompt , { width = 1024 , height = 1024 } = {}) {
50 let lastErr;
51 for ( const model of MODELS ) {
52 try {
53 const r = await req (ctx, "/runwareschemaless/v1/request" , [
54 {
55 taskType: "imageInference" ,
56 taskUUID: randomUUID (), // must be a real UUIDv4 — slugs 400
57 outputType: "URL" ,
58 outputFormat: "PNG" ,
59 positivePrompt: prompt,
60 width,
61 height,
62 model,
63 numberResults: 1 ,
64 },
65 ]);
66 const url = r?.data?.[ 0 ]?.imageURL;
67 if (url) return url;
68 lastErr = new Error ( `no imageURL in response: ${ JSON . stringify ( r ). slice ( 0 , 200 ) }` );
69 } catch (e) {
70 lastErr = e;
71 }
72 }
73 throw lastErr;
74 }
75
76 const MIME = { ".png" : "image/png" , ".jpg" : "image/jpeg" , ".jpeg" : "image/jpeg" , ".webp" : "image/webp" , ".gif" : "image/gif" , ".avif" : "image/avif" };
77
78 /**
79 * Upload a LOCAL file (a path on this machine — the user's own asset) into Wix Media;
80 * returns { id, url } (permanent). Two steps per the Upload API: generate-upload-url, then
81 * PUT the bytes to it — the PUT response carries the file descriptor.
82 * docs: https://dev.wix.com/docs/api-reference/assets/media/media-manager/files/generate-file-upload-url.md
83 */
84 export async function uploadImage ( ctx , path , displayName ) {
85 const ext = extname (path). toLowerCase ();
86 const mimeType = MIME [ext];
87 if ( ! mimeType) throw new Error ( `unsupported image extension: ${ path }` );
88 const bytes = readFileSync (path); // throws loud on a wrong path (caught per-item by resolveItemImages)
89 // fileName's extension MUST match the real file type — a mismatch (slug.png for a .jpg) is
90 // rejected; keep the caller's display name, swap in the file's own extension.
91 const fileName = (displayName ?? basename (path)). replace ( / \. [a-z0-9] +$ / i , "" ) + ext;
92 const { uploadUrl } = await req (ctx, "/site-media/v1/files/generate-upload-url" , {
93 mimeType,
94 fileName,
95 });
96 const res = await fetch (uploadUrl, {
97 method: "PUT" ,
98 headers: { "Content-Type" : mimeType },
99 body: bytes,
100 signal: AbortSignal. timeout ( 120_000 ),
101 });
102 const json = await res. json (). catch (() => ({}));
103 const f = json.file || json;
104 if ( ! res.ok || ! f?.id) throw new Error ( `upload failed (${ res . status }): ${ JSON . stringify ( json ). slice ( 0 , 200 ) }` );
105 return { id: f.id, url: f.url };
106 }
107
108 /** Import an external/generated URL into Wix Media; returns { id, url } (permanent). */
109 // docs: https://dev.wix.com/docs/api-reference/assets/media/media-manager/files/import-file.md
110 export async function importImage ( ctx , url , displayName = "image.png" ) {
111 const r = await req (ctx, "/site-media/v1/files/import" , { url, mimeType: "image/png" , displayName });
112 const f = r.file || r;
113 if ( ! f?.id) throw new Error ( `import-file returned no file id: ${ JSON . stringify ( r ). slice ( 0 , 200 ) }` );
114 return { id: f.id, url: f.url };
115 }
116
117 /**
118 * THE seed entry point. Resolves a batch of image specs to Wix Media files in ONE parallel
119 * wave. Each spec: { path } (LOCAL file — the user's own asset, uploaded) OR { url }
120 * (verified external URL — imported) OR { prompt } (generated, ~1 credit) — plus optional
121 * displayName, width, height. Returns an array aligned with the input: { id, url } per
122 * success, null per failure or empty spec. Never throws.
123 */
124 export async function resolveItemImages ( ctx , specs , { perImageBudgetMs = 120_000 } = {}) {
125 // unref: the budget timer must never keep the seed process alive after the work is done —
126 // a lingering timer delays the seed's exit (and the run's .seed-exit marker) by the budget.
127 const deadline = new Promise (( r ) => {
128 const t = setTimeout (() => r ( null ), perImageBudgetMs);
129 t. unref ?.();
130 });
131 const results = await Promise . allSettled (
132 (specs ?? []). map ( async ( s ) => {
133 if ( ! s || ( ! s.path && ! s.url && ! s.prompt)) return null ;
134 const resolve = ( async () => {
135 if (s.path) return uploadImage (ctx, s.path, s.displayName);
136 const source = s.url ?? ( await generateImage (ctx, s.prompt, { width: s.width, height: s.height }));
137 return importImage (ctx, source, s.displayName ?? "image.png" );
138 })();
139 // Hard per-image budget: even a pathological multi-model hang costs the seed at most
140 // perImageBudgetMs of wall clock (the wave is parallel, so it's paid once, not per item).
141 return Promise . race ([resolve, deadline]);
142 }),
143 );
144 return results. map (( r ) => (r.status === "fulfilled" ? r.value : null ));
145 }