1// MyKit â batch-generate photorealistic look images using Replicate Flux Schnell. 2// Requires a Replicate API token (see Settings). Uses the local same-origin 3// proxy when served by server.js so browsers do not hit Replicate CORS limits. 4// Each image is generated, resized to 512Ã640, and stored as a base64 JPEG in 5// localStorage keyed by look id. 6 7const LookGen = (() => { 8 9 const MODEL = 'black-forest-labs/flux-schnell'; 10 const THUMB_W = 512; 11 const THUMB_H = 640; 12 13 async function generateAll({ replicateKey, onProgress, lookIds }) { 14 if (!replicateKey) throw new Error('Replicate API token required'); 15 16 const targets = lookIds 17 ? LOOKS.filter(l => lookIds.includes(l.id)) 18 : LOOKS; 19 20 let done = 0; 21 const total = targets.length; 22 const errors = []; 23 24 for (const look of targets) { 25 try { 26 onProgress && onProgress({ done, total, currentId: look.id, currentName: look.name }); 27 const dataUrl = await generateOne({ look, replicateKey }); 28 Store.setLookImage(look.id, dataUrl); 29 } catch (err) { 30 console.error(`Failed generating ${look.id}:`, err); 31 errors.push({ id: look.id, error: err.message || String(err) }); 32 } 33 done++; 34 onProgress && onProgress({ done, total, currentId: look.id, currentName: look.name }); 35 } 36 37 return { done, total, errors }; 38 } 39 40 async function generateOne({ look, replicateKey }) { 41 // Using Prefer: wait tells Replicate to hold the connection synchronously 42 // until the prediction finishes (up to 60s). Flux Schnell is fast enough 43 // that this almost always returns in one request. 44 const res = await fetch(replicateUrl(`/v1/models/${MODEL}/predictions`), { 45 method: 'POST', 46 headers: { 47 'Authorization': `Token ${replicateKey}`, 48 'Content-Type': 'application/json', 49 'Prefer': 'wait', 50 }, 51 body: JSON.stringify({ 52 input: { 53 prompt: look.prompt, 54 aspect_ratio: '4:5', 55 num_outputs: 1, 56 output_format: 'jpg', 57 output_quality: 85, 58 num_inference_steps: 4, 59 megapixels: '1', 60 disable_safety_checker: false, 61 }, 62 }), 63 }); 64 65 if (!res.ok) { 66 const text = await res.text(); 67 throw new Error(`Replicate ${res.status}: ${text.slice(0, 200)}`); 68 } 69 let pred = await res.json(); 70 71 // Fallback to polling if Prefer: wait didn't return a finished prediction 72 if (pred.status && pred.status !== 'succeeded' && pred.status !== 'failed') { 73 const pollPath = pred.urls && pred.urls.get 74 ? new URL(pred.urls.get).pathname 75 : `/v1/predictions/${pred.id}`; 76 const start = Date.now(); 77 while (Date.now() - start < 90000) { 78 if (pred.status === 'succeeded') break; 79 if (pred.status === 'failed' || pred.status === 'canceled') { 80 throw new Error('Prediction ' + pred.status + ': ' + (pred.error || '')); 81 } 82 await sleep(1500); 83 const pr = await fetch(replicateUrl(pollPath), { 84 headers: { 'Authorization': `Token ${replicateKey}` }, 85 }); 86 if (!pr.ok) throw new Error('Poll failed: ' + pr.status); 87 pred = await pr.json(); 88 } 89 } 90 if (pred.status !== 'succeeded') throw new Error('Timed out: ' + pred.status); 91 92 const outUrl = Array.isArray(pred.output) ? pred.output[0] : pred.output; 93 if (!outUrl) throw new Error('No output URL'); 94 95 return await fetchAndResize(outUrl); 96 } 97 98 // Fetch the generated image and downscale it so we can fit all looks in 99 // localStorage (~50KB each at 512Ã640 JPEG q72). 100 async function fetchAndResize(url) { 101 const res = await fetch(url); 102 if (!res.ok) throw new Error('CDN fetch ' + res.status); 103 const blob = await res.blob(); 104 const bmp = await createImageBitmap(blob); 105 const canvas = document.createElement('canvas'); 106 canvas.width = THUMB_W; 107 canvas.height = THUMB_H; 108 const ctx = canvas.getContext('2d'); 109 const iR = bmp.width / bmp.height, bR = THUMB_W / THUMB_H; 110 let sx = 0, sy = 0, sw = bmp.width, sh = bmp.height; 111 if (iR > bR) { sw = bmp.height * bR; sx = (bmp.width - sw) / 2; } 112 else { sh = bmp.width / bR; sy = (bmp.height - sh) / 2; } 113 ctx.drawImage(bmp, sx, sy, sw, sh, 0, 0, THUMB_W, THUMB_H); 114 return canvas.toDataURL('image/jpeg', 0.72); 115 } 116 117 function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } 118 119 function replicateUrl(pathOrUrl) { 120 const path = /^https:\/\/api\.replicate\.com\//i.test(pathOrUrl) 121 ? new URL(pathOrUrl).pathname + new URL(pathOrUrl).search 122 : pathOrUrl; 123 if (window.location.protocol === 'file:') return `https://api.replicate.com${path}`;
124 return `/api/replicate${path}`; 125 } 126 127 function missingLooks() { 128 const cached = Store.getLookImages(); 129 return LOOKS.filter(l => !cached[l.id]).map(l => l.id); 130 } 131 132 function stats() { 133 const cached = Store.getLookImages(); 134 const total = LOOKS.length; 135 const done = LOOKS.filter(l => cached[l.id]).length; 136 return { done, total, missing: total - done }; 137 } 138 139 return { generateAll, missingLooks, stats }; 140})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.