CoolFace
Apppublic

radames/OpenAI-CLIP-JavaScript

sourceHugging Faceupdated 3y agoView on Hugging Face
7likes
index.html640 linesDownload Raw Back to root
1<!DOCTYPE html>2<html>3  <head>4    <meta charset="utf-8">5    <meta name="viewport" content="width=device-width">6    <title>OpenAI CLIP Image Search in JavaScript (Using ONNX Web Runtime)</title>7    <script src="enable-threads.js"></script>8    <script src="./vips/vips.js"></script>9  </head>10  <body>11    <style>12      body * {13        font-family: monospace;14      }15    </style>16    <script src="https://cdn.jsdelivr.net/npm/onnxruntime-web@1.12.0/dist/ort.js"></script>17    <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@3.17.0/dist/tf.min.js"></script> <!-- NOTE: tfjs is currently only used for image preprocessing stuff. -->18    19    <div>20      <h1 style="font-size:1rem;">Sort/search images using OpenAI's CLIP in your browser</h1>21      <p>This web app sorts/searches through images in a directory on your computer using OpenAI's CLIP model, and the new File System Access API. <a href="https://github.com/josephrocca/clip-image-sorter">Here's the Github repo</a> for this web app, and <a href="https://github.com/josephrocca/openai-clip-js">here's the Github repo</a> for the web-ported CLIP models. Feel free to open an issue or <a href="https://twitter.com/rocca27" target="_blank">DM me on Twitter</a> if you have any questions about this demo.</p>22      <p>All processing happens in your browser, on your device - i.e. your images are <b>not</b> uploaded to a server for processing.</p>23      <p id="browserCompatibilityWarning" style="padding:0.25rem; background:rgb(255, 227, 160); display:none;"><b>Note</b>: This page uses new browser features (File System Access API, and credentialless COEP) that are currently only available in some browsers. As of writing, it works in Chrome, Edge and Brave. Other browsers like Firefox and Safari are often a bit slower in implementing cutting-edge features.</p>24      <script>25        if(Date.now() < 1648725949710+1000*60*60*24*365) { // display until start of April 202326          browserCompatibilityWarning.style.display = "";27        }28      </script>29      <hr>30      31      <div id="modelNameSelectCtn" style="padding:0.5rem; background:lightgrey; margin:0.5rem;">32        <b>Step 1:</b> Choose model:33        <select onchange="window.MODEL_NAME=this.value;">34          <option value="clip_vit_32">CLIP ViT-B/32 (recommended)</option>35          <option value="clip_vit_32_uint8">CLIP ViT-B/32 (quantized - inaccurate embeddings)</option>36          <option value="lit_b16b">LiT B16B</option>37        </select>38      </div>39      40      <div id="initCtnEl" style="padding:0.5rem; background:lightgrey; margin:0.5rem;">41        <b>Step 2:</b> Download and initialize the models.42        <br>43        Download image model: <progress id="imageModelLoadingProgressBarEl" value="0"></progress> <span id="imageModelLoadingMbEl"></span>44        <br>45        Download text model: <progress id="textModelLoadingProgressBarEl" value="0"></progress> <span id="textModelLoadingMbEl"></span>46        <br>47        Initialize workers: <progress id="workerInitProgressBarEl" value="0"></progress>48        <div style="display:none;"> <!-- more workers (dividing threads between them) doesn't seem to make things faster -->49          Number of image embedding workers/threads: <input id="numThreadsEl" type="range" min="1" max="4" value="1" oninput="numThreadsDisplayEl.textContent=this.value"> <span id="numThreadsDisplayEl"></span> <script>numThreadsEl.max = navigator.hardwareConcurrency; numThreadsDisplayEl.textContent=numThreadsEl.value;</script>50        </div>51        <br>52        <button id="initWorkersBtn" onclick="modelNameSelectCtn.style.pointerEvents='none'; modelNameSelectCtn.style.opacity=0.5; initializeWorkers()">initialize workers</button>53      </div>54 55      <div id="pickDirCtnEl" style="opacity:0.5; pointer-events:none; padding:0.5rem; background:lightgrey; margin:0.5rem;">56        <b>Step 3:</b> Pick a directory of images (images in subdirectories will be included).57        <br>58        <button id="pickDirectoryBtn" onclick="pickDirectory({source:'local'})">pick directory</button> &nbsp;&nbsp;&nbsp;&nbsp;or&nbsp;&nbsp;&nbsp;&nbsp; <button id="useRedditImagesBtn" onclick="pickDirectory({source:'reddit'})">use ~200k reddit images</button> (remove nsfw:<input id="removeRedditNsfwEl" type="checkbox" checked>)59        <br>60        <div id="redditLoadProgressCtn" style="display:none;">Download progress: <progress id="redditProgressBarEl" value="0"></progress> <span id="redditProgressMbEl"></span></div>61        <div id="existingEmbeddingsProgressCtn" style="display:none;">Loading existing embeddings: <span id="existingEmbeddingsLoadedEl">none</span></div>62      </div>63      64      <div id="computeEmbeddingsCtnEl" style="opacity:0.5; pointer-events:none; padding:0.5rem; background:lightgrey; margin:0.5rem;">65        <b>Step 4:</b> Compute image embeddings. <span style="opacity:0.5;">(they will be saved as &lt;ModelName&gt;_embeddings.tsv in the selected directory)</span>66        <br>67        <button id="computeEmbeddingsBtn" onclick="computeImageEmbeddings(); this.disabled=true;">compute image embeddings</button>68        <br>69        <span id="computeEmbeddingsProgressEl">0</span> images embedded (<span id="computeEmbeddingsSpeedEl">?</span> ms per image) <span id="preexistingEmbeddingsEl"></span>70      </div>71 72      <div id="existingEmbeddingsFoundCtnEl" style="display:none; padding:0.5rem; background:lightgrey; margin:0.5rem;">73        <b>Step 5:</b> <b>Existing embeddings found.</b>74        <br>75        Only needed if you've added or changed images: <button onclick="existingEmbeddingsFoundCtnEl.style.display='none'; computeEmbeddingsCtnEl.style.display=''; disableCtn(searchCtnEl); computeEmbeddingsBtn.click()">(re)compute image embeddings</button>76        <input id="onlyEmbedNewImagesCheckbox" type="checkbox" checked> Only new images?77      </div>78 79      <div id="searchCtnEl" style="opacity:0.5; pointer-events:none; padding:0.5rem; background:lightgrey; margin:0.5rem;">80        <b>Step 6:</b> Enter a search term.81        <br>82        <input id="searchTextEl" style="width:300px;" value="" placeholder="Enter search text here..." onkeyup="if(event.which==13) searchSort()">83        <button id="searchBtn" onclick="searchSort()">search</button>84      </div>85    </div>86 87    <hr>88    <b>Results</b> <span style="opacity:0.5;">(hover for cosine similarities)</span>89    <div id="resultsEl" style="margin-top:1rem; min-height:100vh;"><span style="opacity:0.5;">Click the search button to compute the results.</span></div>90    91    <script>92      /////////////93      //  STEP 1 //94      /////////////95      window.MODEL_NAME = "clip_vit_32";96      window.modelData = {97        clip_vit_32: {98          image: {99            modelUrl: (quantized) => `https://huggingface.co/rocca/openai-clip-js/resolve/main/clip-image-vit-32-${quantized ? "uint8" : "float32"}.onnx`,100            embed: async function(blob, session) {101              let rgbData = await getRgbData(blob);102              const feeds = {input: new ort.Tensor('float32', rgbData, [1,3,224,224])};103              const results = await session.run(feeds);104              const embedVec = results["output"].data; // Float32Array105              return embedVec;106            }107          },108          text: {109            modelUrl: (quantized) => `https://huggingface.co/rocca/openai-clip-js/resolve/main/clip-text-vit-32-${quantized ? "uint8" : "float32-int32"}.onnx`,110            embed: async function(text, session) {111              if(!window.textTokenizerClip) {112                let Tokenizer = (await import("https://deno.land/x/clip_bpe@v0.0.6/mod.js")).default;113                window.textTokenizerClip = new Tokenizer(); 114              }115              let textTokens = window.textTokenizerClip.encodeForCLIP(text);116              textTokens = Int32Array.from(textTokens);117              const feeds = {input: new ort.Tensor('int32', textTokens, [1, 77])};118              const results = await session.run(feeds);119              return [...results["output"].data];120            },121          }122        },123        lit_b16b: {124          image: {125            modelUrl: () => 'https://huggingface.co/rocca/lit-web/resolve/main/embed_images.onnx',126            embed: async function(blob, session) {127              128              // TODO: Maybe remove tf from this code so you can remove the whole tfjs dependency129              blob = await bicubicResizeAndCenterCrop(blob);130              let inputImg = new Image();131              await new Promise(r => inputImg.onload=r, inputImg.src=URL.createObjectURL(blob));132              let img = tf.browser.fromPixels(inputImg);133              img = tf.sub(tf.div(tf.expandDims(img), 127.5), 1);134              let float32RgbData = img.dataSync();135              136              const feeds = {'images': new ort.Tensor('float32', float32RgbData, [1,224,224,3])};137              const results = await session.run(feeds);138              return results["Identity_1:0"].data;139            },140          },141          text: {142            modelUrl: () => 'https://huggingface.co/rocca/lit-web/resolve/main/embed_text_tokens.onnx',143            embed: async function(text, session) {144              if(!window.bertTextTokenizerLit) {145                window.bertTextTokenizerLit = await import("./bert-text-tokenizer.js").then(m => new m.BertTokenizer());146                await window.bertTextTokenizerLit.load();147              }148              let textTokens = window.bertTextTokenizerLit.tokenize(text);149              textTokens.unshift(101); // manually put CLS token at the start150              textTokens.length = 16;151              textTokens = [...textTokens.slice(0, 16)].map(e => e == undefined ? 0 : e); // pad with zeros to length of 16152              textTokens = Int32Array.from(textTokens);153              const feeds = {'text_tokens': new ort.Tensor('int32', textTokens, [1,16])};154              const results = await session.run(feeds);155              return [...results["Identity_1:0"].data];156            }157          }158        },159      };160      let imageWorkers = [];161      let onnxImageSessions = [];162      let onnxTextSession;163      let textTokenizer;164      async function initializeWorkers() {165        initWorkersBtn.disabled = true;166        numThreadsEl.disabled = true;167        168        let useQuantizedModel = false;169        170        if(MODEL_NAME.endsWith("_uint8")) {171          MODEL_NAME = MODEL_NAME.replace(/_uint8$/g, "");172          useQuantizedModel = true;173        }174        175        let imageOnnxBlobPromise = downloadBlobWithProgress(window.modelData[MODEL_NAME].image.modelUrl(useQuantizedModel), function(e) {176          let ratio = e.loaded / e.total;177          imageModelLoadingProgressBarEl.value = ratio;178          imageModelLoadingMbEl.innerHTML = Math.round(ratio*e.total/1e6)+" MB";179        });180 181        let textOnnxBlobPromise = downloadBlobWithProgress(window.modelData[MODEL_NAME].text.modelUrl(useQuantizedModel), function(e) {182          let ratio = e.loaded / e.total;183          textModelLoadingProgressBarEl.value = ratio;184          textModelLoadingMbEl.innerHTML = Math.round(ratio*e.total/1e6)+" MB";185        });186 187        let [imageOnnxBlob, textOnnxBlob] = await Promise.all([imageOnnxBlobPromise, textOnnxBlobPromise])188 189        let imageModelUrl = window.URL.createObjectURL(imageOnnxBlob);190        let textModelUrl = window.URL.createObjectURL(textOnnxBlob);191        192        let numImageWorkers = Number(numThreadsEl.value);193        194        // Inference latency is about 5x faster with wasm threads, but this requires these headers: https://web.dev/coop-coep/ I'm using this as a hack (in enable-threads.js) since Github pages doesn't allow setting headers: https://github.com/gzuidhof/coi-serviceworker195        if(self.crossOriginIsolated) {196          ort.env.wasm.numThreads = Math.ceil(navigator.hardwareConcurrency / numImageWorkers) / 2; // divide by two to utilise only half the CPU's threads because trying to use all the cpu's threads actually makes it slower197        }198 199        workerInitProgressBarEl.max = numImageWorkers + 2; // +2 because of text model and bpe library200        201        let imageModelExecutionProviders = ["wasm"]; // webgl is not compatible with this model (need to tweak conversion data/op types)202 203        for(let i = 0; i < numImageWorkers; i++) {204          let session = await ort.InferenceSession.create(imageModelUrl, { executionProviders: imageModelExecutionProviders }); 205          onnxImageSessions.push(session);206          imageWorkers.push({207            session,208            busy: false,209          });210          workerInitProgressBarEl.value = Number(workerInitProgressBarEl.value) + 1;211        }212        console.log("Image model loaded.");213 214        onnxTextSession = await ort.InferenceSession.create(textModelUrl, { executionProviders: ["wasm"] }); // webgl is not compatible with this model (need to tweak conversion data/op types)215        console.log("Text model loaded.");216        workerInitProgressBarEl.value = Number(workerInitProgressBarEl.value) + 1;217 218        window.URL.revokeObjectURL(imageModelUrl);219        window.URL.revokeObjectURL(textModelUrl);220 221        window.vips = await Vips(); // for bicubicly resizing images (since that's what CLIP expects)222        window.vips.EMBIND_AUTOMATIC_DELETELATER = false;223 224        workerInitProgressBarEl.value = Number(workerInitProgressBarEl.value) + 1;225 226        disableCtn(initCtnEl);227        enableCtn(pickDirCtnEl);228      }229 230 231      /////////////232      //  STEP 2 //233      /////////////234      let directoryHandle;235      let embeddingsFileHandle;236      let embeddings;237      let dataSource;238      async function pickDirectory(opts={}) {239        dataSource = opts.source;240        241        if(dataSource === "local") {242          if(!window.showDirectoryPicker) return alert("Your browser does not support some modern features (specifically, File System Access API) required to use this web app. Please try updating your browser, or switching to Chrome, Edge, or Brave.");243          directoryHandle = await window.showDirectoryPicker();244          embeddingsFileHandle = await directoryHandle.getFileHandle(`${window.MODEL_NAME}_embeddings.tsv`, {create:true});245          246          pickDirectoryBtn.disabled = true;247          useRedditImagesBtn.disabled = true;248          pickDirectoryBtn.textContent = "Loading...";249        }250        251        let redditEmbeddingsBlob;252        if(dataSource === "reddit") {253          if(window.MODEL_NAME !== "clip_vit_32") return alert("Sorry, there are only pre-computed Reddit image embeddings for the CLIP ViT-B/32 model at the moment.");254          if(!removeRedditNsfwEl.checked && !confirm("Are you sure you'd like to see NSFW Reddit images?")) return;255          if(removeRedditNsfwEl.checked) alert("Note that NSFW images are filtered from Reddit using CLIP, and CLIP can make mistakes, so some NSFW images may still be shown.");256            257          pickDirectoryBtn.disabled = true;258          useRedditImagesBtn.disabled = true;259          useRedditImagesBtn.textContent = "Loading...";260          redditLoadProgressCtn.style.display = "";261          262          redditEmbeddingsBlob = await downloadBlobWithProgress("https://huggingface.co/datasets/rocca/top-reddit-posts/resolve/main/clip_embeddings_top_50_images_per_subreddit.tsv.gz", function(e) {263            let ratio = e.loaded / e.total;264            redditProgressBarEl.value = ratio;265            redditProgressMbEl.innerHTML = Math.round(ratio*213)+" MB";266          });267        }268        269        try {270          existingEmbeddingsProgressCtn.style.display = "";271          272          embeddings = {};273          let file, opts;274          if(dataSource === "local") {275            file = await embeddingsFileHandle.getFile();276            opts = {};277          }278          if(dataSource === "reddit") {279            file = redditEmbeddingsBlob;280            opts = {decompress:"gzip"};281          }282          283          let i = 0;284          for await (let line of makeTextFileLineIterator(file, opts)) {285            if(!line || !line.trim()) continue; // <-- to skip final new line (not sure if this is needed)286            let [filePath, embeddingVec] = line.split("\t");287            embeddings[filePath] = JSON.parse(embeddingVec);288            i++;289            if(i % 1000 === 0) {290              existingEmbeddingsLoadedEl.innerHTML = i;291              await sleep(10);292            }293          }294        } catch(e) {295          embeddings = undefined;296          console.log("No existing embedding found, or the embeddings file was corrupted:", e);297          existingEmbeddingsProgressCtn.style.display = "none";298        }299        300        pickDirectoryBtn.textContent = "Done.";301        useRedditImagesBtn.textContent = "Done.";302 303        disableCtn(pickDirCtnEl);304        enableCtn(computeEmbeddingsCtnEl);305        enableCtn(searchCtnEl);306 307        if(embeddings && Object.keys(embeddings).length > 0) {308          computeEmbeddingsCtnEl.style.display = "none";309          existingEmbeddingsFoundCtnEl.style.display = "";310        }311        312        if(dataSource === "reddit") {313          disableCtn(existingEmbeddingsFoundCtnEl);314        }315        316      }317      318 319      /////////////320      //  STEP 3 //321      /////////////322      let totalEmbeddingsCount = 0;323      let imagesEmbedded;324      let recentEmbeddingTimes = []; // how long each embed took in ms, newest at end325      let recomputeAllEmbeddings;326      let imagesBeingProcessedNow = 0;327      let needToSaveEmbeddings = false;328      async function computeImageEmbeddings() {329        imagesEmbedded = 0;330        totalEmbeddingsCount = Object.keys(embeddings).length;331 332        recomputeAllEmbeddings = !onlyEmbedNewImagesCheckbox.checked;333        let gotSomeExistingEmbeddings = totalEmbeddingsCount > 0;334        335        if(onlyEmbedNewImagesCheckbox.checked && gotSomeExistingEmbeddings) {336          preexistingEmbeddingsEl.innerHTML = `(loaded ${Object.keys(embeddings).length} existing embeddings)`; 337        }338 339        if(recomputeAllEmbeddings || !gotSomeExistingEmbeddings) {340          embeddings = {}; // <-- maps file path (relative to top/selected directory) to embedding341        }342        343        try {344          await recursivelyProcessImagesInDir(directoryHandle);345          await saveEmbeddings();346        } catch(e) {347          console.error(e);348          alert(e.message);349        }350 351        disableCtn(computeEmbeddingsCtnEl);352        enableCtn(searchCtnEl);353      }354      async function recursivelyProcessImagesInDir(dirHandle, currentPath="") {355        for await (let [name, handle] of dirHandle) {356          const {kind} = handle;357          let path = `${currentPath}/${name}`;358          if (handle.kind === 'directory') {359            await recursivelyProcessImagesInDir(handle, path);360          } else {361            let isImage = /\.(png|jpg|jpeg|webp)$/.test(path);362            if(!isImage) continue;363 364            let alreadyGotEmbedding = !!embeddings[path];365            if(alreadyGotEmbedding && !recomputeAllEmbeddings) continue;366            367            if(needToSaveEmbeddings) {368              await saveEmbeddings();369              needToSaveEmbeddings = false;370            }371              372            while(imageWorkers.filter(w => !w.busy).length === 0) await sleep(1);373            374            let worker = imageWorkers.filter(w => !w.busy)[0];375            worker.busy = true;376            imagesBeingProcessedNow++;377 378            (async function() {379              let startTime = Date.now();380              381              let blob = await handle.getFile();382              const embedVec = await modelData[MODEL_NAME].image.embed(blob, worker.session);383 384              embeddings[path] = [...embedVec];385              worker.busy = false;386 387              imagesEmbedded++;388              totalEmbeddingsCount++;389              390              computeEmbeddingsProgressEl.innerHTML = imagesEmbedded;391              392              let saveInterval = totalEmbeddingsCount > 50_000 ? 10_000 : 1000; // since saves take longer if there are lots of embeddings393              if(imagesEmbedded % saveInterval === 0) {394                needToSaveEmbeddings = true;395              }396              397              recentEmbeddingTimes.push(Date.now()-startTime);398              if(recentEmbeddingTimes.length > 100) recentEmbeddingTimes = recentEmbeddingTimes.slice(-50);399              if(recentEmbeddingTimes.length > 10) computeEmbeddingsSpeedEl.innerHTML = Math.round(recentEmbeddingTimes.slice(-20).reduce((a,v) => a+v, 0)/20);400 401              imagesBeingProcessedNow--;402            })();403          }404        }405        while(imagesBeingProcessedNow > 0) await sleep(10);406      }407      408 409      /////////////410      //  STEP 4 //411      /////////////412      async function searchSort() {413        searchBtn.disabled = true;414        415        if(dataSource === "local") {416          for(let imgEl of [...document.querySelectorAll("img")]) {417            URL.revokeObjectURL(imgEl.src);418          }419        }420        421        resultsEl.innerHTML = "Loading...";422        await sleep(50);423 424        let searchTextEmbedding = await modelData[MODEL_NAME].text.embed(searchTextEl.value, onnxTextSession);425        let similarities = {};426        for(let [path, embedding] of Object.entries(embeddings)) {427          similarities[path] = cosineSimilarity(searchTextEmbedding, embedding);428        }429        let similarityEntries = Object.entries(similarities).sort((a,b) => b[1]-a[1]).slice(0, 5000);430 431        if(dataSource === "reddit" && removeRedditNsfwEl.checked) {432          let nsfwTextEmbedding = await modelData[MODEL_NAME].text.embed(atob('cG9ybiBuYWtlZCBwZW5pcyB2YWdpbmEgbnVkZSBzZXggZGljayBwdXNzeSBzZXh1YWwgcG9ybm9ncmFwaGljIGFzcyBib29icw=='), onnxTextSession); // nsfw words (hidden with `btoa`)433          let nsfwSimilarities = {};434          for(let [path, similarity] of similarityEntries) {435            let embedding = embeddings[path];436            nsfwSimilarities[path] = cosineSimilarity(nsfwTextEmbedding, embedding);437          }438          similarityEntries = similarityEntries.filter(e => nsfwSimilarities[e[0]] < 0.2093);439        }440        441        let resultHtml = "";442        let numResults = 0;443        for(let [path, score] of similarityEntries.slice(0, 500)) {444          if(dataSource === "local") {445            let handle = await getFileHandleByPath(path);446            let url = URL.createObjectURL(await handle.getFile());447            resultHtml += `<img src="${url}" style="max-height:400px;" title="${path}: ${score}" loading="lazy"/>`;448          }449          if(dataSource === "reddit") {450            let imageUrl = `https://i.redd.it/${path.split("__")[1]}`;451            let postUrl = `https://reddit.com/comments/${path.split("__")[0].split("/")[1]}`;452            resultHtml += `<a href="${postUrl}" target="_blank"><img src="${imageUrl}" onload="this.style.height='';this.style.width='';this.style.border='';" style="max-height:400px; height:300px; width:300px; border:1px solid black;" title="${path}: ${score}" loading="lazy"/></a>`;453          }454          numResults++;455        }456        457        if(!resultHtml) {458          resultsEl.innerHTML = "No results found after filtering NSFW.";459        } else {460          resultsEl.innerHTML = resultHtml;461        }462        463        searchBtn.disabled = false;464      }465 466 467 468 469      /////////////////////////////470      //  FUNCTIONS / UTILITIES  //471      /////////////////////////////472 473      async function getFileHandleByPath(path) {474        let handle = directoryHandle;475        let chunks = path.split("/").slice(1);476        for(let i = 0; i < chunks.length; i++) {477          let chunk = chunks[i];478          if(i === chunks.length-1) {479            handle = await handle.getFileHandle(chunk);480          } else {481            handle = await handle.getDirectoryHandle(chunk);482          }483        }484        return handle;485      }486      487      async function getRgbData(blob) {488        // let blob = await fetch(imgUrl, {referrer:""}).then(r => r.blob());489 490        let resizedBlob = await bicubicResizeAndCenterCrop(blob);491        let img = await createImageBitmap(resizedBlob);492 493        let canvas = new OffscreenCanvas(224, 224);494        let ctx = canvas.getContext("2d");495        ctx.drawImage(img, 0, 0);496        let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);497 498        let rgbData = [[], [], []]; // [r, g, b]499        // remove alpha and put into correct shape:500        let d = imageData.data;501        for(let i = 0; i < d.length; i += 4) { 502          let x = (i/4) % canvas.width;503          let y = Math.floor((i/4) / canvas.width)504          if(!rgbData[0][y]) rgbData[0][y] = [];505          if(!rgbData[1][y]) rgbData[1][y] = [];506          if(!rgbData[2][y]) rgbData[2][y] = [];507          rgbData[0][y][x] = d[i+0]/255;508          rgbData[1][y][x] = d[i+1]/255;509          rgbData[2][y][x] = d[i+2]/255;510          // From CLIP repo: Normalize(mean=(0.48145466, 0.4578275, 0.40821073), std=(0.26862954, 0.26130258, 0.27577711))511          rgbData[0][y][x] = (rgbData[0][y][x] - 0.48145466) / 0.26862954;512          rgbData[1][y][x] = (rgbData[1][y][x] - 0.4578275) / 0.26130258;513          rgbData[2][y][x] = (rgbData[2][y][x] - 0.40821073) / 0.27577711;514        }515        rgbData = Float32Array.from(rgbData.flat().flat());516        return rgbData;517      }518      519      async function bicubicResizeAndCenterCrop(blob) {520        let im1 = vips.Image.newFromBuffer(await blob.arrayBuffer());521 522        // Resize so smallest side is 224px:523        const scale = 224 / Math.min(im1.height, im1.width);524        let im2 = im1.resize(scale, { kernel: vips.Kernel.cubic });525 526        // crop to 224x224:527        let left = (im2.width - 224) / 2;528        let top = (im2.height - 224) / 2;529        let im3 = im2.crop(left, top, 224, 224)530 531        let outBuffer = new Uint8Array(im3.writeToBuffer('.png'));532        im1.delete(), im2.delete(), im3.delete();533        return new Blob([outBuffer], { type: 'image/png' });534      }535 536 537      function downloadBlobWithProgress(url, onProgress) {538        return new Promise((res, rej) => {539          var blob;540          var xhr = new XMLHttpRequest();541          xhr.open('GET', url, true);542          xhr.responseType = 'arraybuffer';543          xhr.onload = function(e) {544            blob = new Blob([this.response]);   545          };546          xhr.onprogress = onProgress;547          xhr.onloadend = function(e){548            res(blob);549          }550          xhr.send();551        });552      }553 554      async function saveEmbeddings(opts={}) {555        let writable = await embeddingsFileHandle.createWritable();556        let textBatch = "";557        let i = 0;558        for(let [filePath, embeddingVec] of Object.entries(embeddings)) {559          let vecString = opts.compress ? JSON.stringify(embeddingVec.map(n => n.toFixed(3))).replace(/"/g, "") : JSON.stringify(embeddingVec);560          textBatch += `${filePath}\t${vecString}\n`;561          i++;562          if(i % 1000 === 0) {563            await writable.write(textBatch);564            textBatch = "";565          }566        }567        await writable.write(textBatch);568        await writable.close();569      }570      571      // Tweaked version of example from here: https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamDefaultReader/read572      async function* makeTextFileLineIterator(blob, opts={}) {573        const utf8Decoder = new TextDecoder("utf-8");574        let stream = await blob.stream();575        576        if(opts.decompress === "gzip") stream = stream.pipeThrough(new DecompressionStream("gzip"));577        578        let reader = stream.getReader();579        580        let {value: chunk, done: readerDone} = await reader.read();581        chunk = chunk ? utf8Decoder.decode(chunk, {stream: true}) : "";582 583        let re = /\r\n|\n|\r/gm;584        let startIndex = 0;585 586        while (true) {587          let result = re.exec(chunk);588          if (!result) {589            if (readerDone) {590              break;591            }592            let remainder = chunk.substr(startIndex);593            ({value: chunk, done: readerDone} = await reader.read());594            chunk = remainder + (chunk ? utf8Decoder.decode(chunk, {stream: true}) : "");595            startIndex = re.lastIndex = 0;596            continue;597          }598          yield chunk.substring(startIndex, result.index);599          startIndex = re.lastIndex;600        }601        if (startIndex < chunk.length) {602          // last line didn't end in a newline char603          yield chunk.substr(startIndex);604        }605      }606 607      function cosineSimilarity(A, B) {608        if(A.length !== B.length) throw new Error("A.length !== B.length");609        let dotProduct = 0, mA = 0, mB = 0;610        for(let i = 0; i < A.length; i++){611          dotProduct += A[i] * B[i];612          mA += A[i] * A[i];613          mB += B[i] * B[i];614        }615        mA = Math.sqrt(mA);616        mB = Math.sqrt(mB);617        let similarity = dotProduct / (mA * mB);618        return similarity;619      }620 621      function sleep(ms) {622        return new Promise(r => setTimeout(r, ms));623      }624 625      function enableCtn(el) {626        el.style.opacity = 1;627        el.style.pointerEvents = "";628      }629      function disableCtn(el) {630        el.style.opacity = 0.5;631        el.style.pointerEvents = "none";632      }633 634      // From the PyTorch model running on CUDA:635      // Text: "a portrait of an astronaut with the American flag" 636      // Embedding: [-1.6626e-01,  5.2277e-02, -1.5332e-01,  4.4946e-01,  2.0667e-01, -2.9565e-01,  4.0588e-02, -4.1016e-01, -1.5027e-01,  3.1934e-01, -6.9702e-02, -2.5488e-01,  1.2335e-01, -9.5337e-02,  2.4109e-01, -4.8950e-02,  2.6074e-01,  5.3835e-04,  2.1033e-01,  3.7012e-01, 4.5679e-01,  3.9795e-01,  3.1641e-01,  3.9551e-01,  1.3931e-02, -4.3060e-02,  4.8798e-02,  3.7158e-01,  1.1731e-01, -3.7256e-01, -2.7295e-01,  3.3130e-01,  5.4980e-01, -2.9816e-02, -2.5806e-01, -1.0016e-01,  8.0750e-02, -6.7139e-02, -2.4072e-01,  2.4353e-01, -3.2202e-01, -1.0327e-01,  1.1566e-01,  6.2646e-01,  1.8262e-01, 2.7539e-01, -1.1816e-01,  4.9512e-01,  8.9539e-02,  5.6299e-01, 2.1313e-01, -1.5625e-01,  1.9958e-01, -5.0049e-01, -2.5854e-01, -4.0430e-01, -1.1298e-01, -6.6338e-03,  2.5391e-01, -5.0629e-02, 2.2253e-01, -2.7295e-01, -5.8289e-03, -4.8804e-01, -7.7820e-02, -3.5187e-02, -3.7537e-02,  4.3213e-01,  3.8300e-02,  2.1045e-01, -3.0347e-01, -9.8999e-02, -1.7407e-01,  2.8882e-01,  1.1322e-01, -1.0883e-01,  1.7065e-01, -2.1191e-01,  1.7920e-01, -1.2805e-01, -4.6924e-01,  1.1957e-01, -1.1829e-01, -1.1902e-01, -2.4353e-01, -9.6008e-02,  2.2913e-01, -1.0948e-02, -1.5686e-01, -2.0483e-01, -2.4756e-01,  9.1125e-02, -9.5557e-01, -4.2511e-02,  4.6356e-02, 4.3481e-01,  2.3633e-01, -3.3252e-01,  3.7231e-01, -5.5695e-02, 7.1777e-02, -1.0370e-01, -2.1912e-01, -1.3733e-01,  1.2048e-01, 1.7151e-01, -1.2659e-01,  2.3523e-01,  2.6001e-01, -4.0381e-01, 1.1761e-01, -4.1626e-02,  1.0974e-01, -5.5206e-02,  4.9713e-02, 5.2197e-01,  3.9124e-02,  5.7959e-01,  9.9609e-02, -3.3740e-01, -2.7295e-01, -7.3389e-01,  1.0962e-01, -3.2178e-01,  6.5869e-01, 2.3460e-03,  2.6733e-02,  3.2471e-02, -2.4500e-01,  7.9041e-02, 1.5405e-01, -3.5547e-01, -1.5625e-01,  4.2695e+00, -1.6113e-01, 1.6467e-01, -3.8794e-01,  2.1545e-02,  1.5771e-01,  2.0068e-01, -3.4741e-01,  2.5244e-01,  1.2201e-01, -3.9795e-02,  3.2471e-01, 2.6562e-01,  1.2915e-01,  1.4465e-02, -2.1265e-01, -2.5055e-02, 5.6689e-01, -6.5125e-02,  1.1652e-01, -5.1025e-01,  1.0712e-01, 8.8867e-02,  1.6882e-01, -6.5125e-02,  4.5929e-02,  2.9517e-01, 6.5479e-01, -9.0881e-02, -4.6417e-02, -3.0713e-01, -5.1537e-03, 4.9390e-01,  5.4492e-01, -3.1812e-01, -6.0577e-02,  3.6591e-02, 7.6782e-02, -3.5181e-01,  4.9487e-01, -4.1040e-01,  4.3970e-01, 1.6553e-01, -3.7329e-01,  3.3594e-01, -1.9263e-01, -1.8225e-01, -1.9623e-02, -4.0454e-01,  1.6187e-01,  1.5259e-01,  2.5122e-01, 1.1993e-01, -8.4595e-02, -4.1016e-01, -1.8225e-01,  1.8555e-01, -3.9124e-02,  2.5122e-01, -2.2949e-01,  5.3125e-01, -1.0504e-01, -2.1439e-02, -2.2559e-01,  5.9357e-02, -3.9160e-01, -3.3716e-01, -9.0393e-02, -1.7493e-01, -2.5952e-01,  2.3401e-01,  3.8013e-01, 1.2927e-01,  1.5491e-01,  1.1920e-01, -1.5906e-01,  5.7487e-03, 7.6172e-02,  1.5552e-01,  2.1790e-01,  9.9304e-02, -3.8025e-02, -1.1829e-01, -1.3293e-01, -2.2278e-01, -2.1472e-01,  3.0957e-01, -1.0254e-01, -2.0264e-01, -2.6840e-02,  8.8379e-02, -8.2092e-02, 1.3647e-01, -2.1399e-01, -2.5684e-01,  1.3745e-01,  6.1371e-02, -1.2988e-01,  6.2683e-02,  1.2964e-01, -3.3112e-02,  1.4111e-01, -2.6440e-01,  4.7379e-03, -1.0815e-01, -4.4971e-01, -3.5583e-02, 1.1469e-01,  6.7871e-02, -5.8350e-02,  1.0297e-01, -6.3086e-01, -8.3350e-01,  4.3481e-01, -1.7383e-01, -1.5491e-01, -5.5176e-01, 5.9766e-01, -2.6880e-01,  5.6976e-02, -2.6318e-01, -4.0466e-02, 2.4927e-01, -1.4893e-01, -2.0032e-01, -2.7515e-01, -1.2598e-01, 3.2440e-02, -1.2939e-01,  6.8018e-01, -3.6060e-01, -3.3496e-01, 9.8267e-02, -1.0010e-01,  1.9653e-01, -3.3032e-01, -2.0032e-01, 1.9116e-01,  8.2214e-02,  3.2397e-01,  1.3708e-01,  3.0899e-02, 1.5454e-01,  3.2422e-01,  1.0693e-01,  3.1714e-01,  3.7280e-01, -2.3401e-01, -5.0171e-02,  7.6758e-01,  2.2046e-01,  3.3997e-02, 1.0231e-02, -2.3999e-01, -1.3672e-01,  5.2002e-01, -6.5552e-02, 6.9275e-02,  2.3706e-01, -3.8257e-01,  1.4990e-01, -2.2656e-01, 4.3018e-01, -4.4678e-01,  2.5977e-01,  3.8208e-01,  9.5215e-02, -2.0239e-01, -2.3169e-01, -3.1396e-01,  3.2806e-02, -2.7905e-01, -1.0361e+00,  3.8147e-02, -1.0278e-01,  2.8369e-01, -3.8623e-01, -2.3132e-01,  1.5784e-01,  4.2734e+00, -1.5820e-01, -1.4758e-01, 2.0032e-01, -1.9678e-01, -7.5586e-01,  1.0712e-01, -2.1777e-01, -4.0552e-01, -1.0919e-01,  1.1932e-01,  7.3486e-01, -1.7700e-01, 5.0732e-01, -9.7351e-02, -4.7192e-01,  5.9052e-02, -7.0117e-01, -1.8774e-01,  2.4329e-01,  3.5083e-01, -2.3633e-01,  2.2205e-01, -2.4719e-03,  4.4507e-01, -1.1005e-01,  5.6494e-01, -1.4697e-01, -8.5022e-02, -2.6050e-01, -8.3679e-02,  3.0615e-01,  1.0181e-01, -2.2266e-01, -1.2054e-01,  1.0094e-02, -1.6382e-01,  3.7671e-01, 1.1371e-01,  8.8959e-03, -1.2866e-01,  7.5500e-02, -3.1177e-01, 9.0698e-02,  2.4139e-02,  2.7124e-01,  4.1382e-01,  9.3872e-02, -3.5815e-01, -1.0602e-01, -2.7637e-01,  1.5613e-01,  2.6025e-01, -1.3428e-01, -2.1509e-01, -3.0005e-01,  7.7515e-02, -2.2253e-01, -1.2634e-01,  1.0114e-01,  2.9395e-01,  4.2065e-01, -4.6425e-03, -6.3721e-01, -4.0308e-01, -5.1849e-02, -9.1309e-02,  1.0577e-01, -1.6800e-02, -4.4823e-03,  2.4231e-01, -1.3635e-01,  1.7041e-01, -9.9243e-02, -1.2439e-01,  1.5247e-01,  1.4717e-02, -1.6785e-01, -3.0615e-01,  2.6074e-01,  1.0938e-01,  4.9487e-01,  1.0529e-01, 3.1799e-02,  7.5928e-02, -1.1212e-01, -3.1201e-01,  5.8740e-01, -1.3171e-01, -1.1090e-01,  5.8887e-01, -1.1420e-01, -2.0056e-01, 1.0425e-01,  2.7710e-01, -5.8098e-03,  5.7324e-01, -1.4417e-01, 1.4575e-01, -2.7466e-01, -2.1313e-01, -1.7627e-01,  1.5466e-01, 3.8013e-01, -1.4612e-01, -2.7246e-01, -1.8604e-01,  1.0394e-01, 1.6016e-01, -1.1017e-01,  1.8140e-01, -3.0078e-01,  6.0303e-01, -1.3904e-01,  1.7322e-01,  2.2510e-01,  2.3303e-01, -5.0879e-01, -2.3462e-01, -2.0544e-01, -2.4768e-01, -2.4121e-01, -7.2754e-01, 7.2754e-01, -4.4312e-02, -1.4198e-02, -1.1475e-01, -1.3684e-01, -3.5278e-01,  1.2347e-01, -2.9602e-02,  4.0550e-03,  1.1951e-01, -6.5575e-03, -7.1228e-02, -3.8062e-01,  6.5125e-02, -1.6541e-01, -3.0289e-02,  8.6609e-02, -1.2134e-01, -1.2164e-01,  3.0319e-02, -1.5173e-01, -7.9834e-02,  1.4148e-01,  2.7319e-01, -2.1545e-01, -1.6382e-01, -2.9419e-01, -2.6611e-01, -7.5102e-04,  1.3135e-01, 9.8389e-02,  3.1812e-01,  5.5115e-02, -5.3253e-02,  4.3823e-02, 3.6957e-02, -1.3599e-01,  1.1023e-01,  9.8267e-02,  2.1643e-01, 9.3567e-02,  1.2718e-02,  1.6406e-01, -1.0338e-02,  1.9019e-01, 1.4392e-01,  3.3081e-02,  1.0138e-01, -1.6943e-01,  8.6136e-03, 1.4478e-01,  1.0941e-02,  1.3635e-01, -8.1543e-01, -3.4912e-01, 7.4959e-03,  2.1997e-01, -2.5681e-02,  2.3206e-01,  3.7622e-01, 3.6401e-01, -1.6357e-01, -2.0984e-01, -1.3220e-01, -6.7322e-02, 2.0117e-01, -4.7583e-01,  6.8054e-02,  2.2437e-01,  2.6709e-01, -5.4626e-02, -4.0741e-02,  5.2002e-02, -1.8872e-01,  3.1372e-01, -1.3574e-01, -2.6538e-01];637    </script>638  </body>639</html>640