ब्राउज़र में कैश एआई मॉडल को कैश मेमोरी में सेव करना

ज़्यादातर एआई मॉडल में कम से कम एक बात समान होती है: वे इंटरनेट पर ट्रांसफ़र किए गए संसाधन के लिए काफ़ी बड़े होते हैं. सबसे छोटे MediaPipe ऑब्जेक्ट डिटेक्शन मॉडल (SSD MobileNetV2 float16) का वज़न 5.6 एमबी है और सबसे बड़े मॉडल का साइज़ 25 एमबी है.

ओपन सोर्स एलएलएम gemma-2b-it-gpu-int4.bin की रफ़्तार 1.35 जीबी है—और एलएलएम के लिए यह बहुत छोटा है. जनरेटिव एआई मॉडल का इस्तेमाल कई तरह से किया जा सकता है. यही वजह है कि आज-कल एआई का इस्तेमाल क्लाउड पर ज़्यादा होता है. ऐप्लिकेशन में, सीधे तौर पर डिवाइस पर ही बहुत ज़्यादा ऑप्टिमाइज़ किए गए मॉडल इस्तेमाल किए जा रहे हैं. हालांकि, ब्राउज़र में चल रहे एलएलएम के डेमो मौजूद हैं, लेकिन यहां ब्राउज़र में चल रहे अन्य मॉडल के प्रोडक्शन-ग्रेड के कुछ उदाहरण दिए गए हैं:

Adobe Photoshop, एआई की मदद से काम करने वाला ऑब्जेक्ट चुनने का टूल वेब पर दिखाता है. इसमें तीन ऑब्जेक्ट चुने गए हैं: दो जिराफ़ और एक चांद.

आने वाले समय में अपने ऐप्लिकेशन को तेज़ी से लॉन्च करने के लिए, आपको इंप्लिसिट एचटीटीपी ब्राउज़र कैश पर निर्भर रहने के बजाय, डिवाइस पर मौजूद मॉडल के डेटा को साफ़ तौर पर कैश मेमोरी में सेव करना चाहिए.

इस गाइड में चैटबॉट बनाने के लिए gemma-2b-it-gpu-int4.bin model का इस्तेमाल किया गया है. हालाँकि, इस तरीके को अन्य मॉडल और डिवाइस पर इस्तेमाल के अन्य उदाहरणों के हिसाब से तय किया जा सकता है. किसी ऐप्लिकेशन को मॉडल से कनेक्ट करने का सबसे सामान्य तरीका यह है कि उस मॉडल को ऐप्लिकेशन के बाकी संसाधनों के साथ दिखाया जाए. डिलीवरी को ऑप्टिमाइज़ करना ज़रूरी है.

सही कैश हेडर कॉन्फ़िगर करना

अगर सर्वर से एआई मॉडल का इस्तेमाल किया जाता है, तो सही Cache-Control हेडर को कॉन्फ़िगर करना ज़रूरी है. नीचे दिए गए उदाहरण में एक ठोस डिफ़ॉल्ट सेटिंग दिखाई गई है, जिसे अपने ऐप्लिकेशन की ज़रूरत के हिसाब से बनाया जा सकता है.

Cache-Control: public, max-age=31536000, immutable

एआई मॉडल का रिलीज़ किया गया हर वर्शन, एक स्टैटिक रिसॉर्स होता है. जिस कॉन्टेंट में कभी बदलाव नहीं होता उसे अनुरोध के यूआरएल में लंबी समयावधि max-age के साथ कैश बस्टिंग कैटगरी में रखा जाना चाहिए. अगर आपको मॉडल अपडेट करना है, तो इसे एक नया यूआरएल दें.

जब उपयोगकर्ता पेज को फिर से लोड करता है, तो क्लाइंट दोबारा पुष्टि करने का अनुरोध भेजता है. भले ही, सर्वर को पता हो कि कॉन्टेंट ठीक से काम कर रहा है. immutable के डायरेक्टिव में साफ़ तौर पर बताया गया है कि दोबारा पुष्टि करना ज़रूरी नहीं है, क्योंकि कॉन्टेंट में कोई बदलाव नहीं होगा. immutable डायरेक्टिव, ब्राउज़र और इंटरमीडियरी कैश या प्रॉक्सी सर्वर पर ज़्यादा काम नहीं करता. हालांकि, दुनिया भर में समझे जाने वाले max-age डायरेक्टिव के साथ इसे जोड़कर, यह पक्का किया जा सकता है कि यह सभी के लिए एक जैसा काम करे. public रिस्पॉन्स डायरेक्टिव से पता चलता है कि रिस्पॉन्स को शेयर की गई कैश मेमोरी में सेव किया जा सकता है.

Chrome DevTools, प्रोडक्शन के Cache-Control हेडर दिखाता है. ये हेडर, हगिंग फ़ेस के ज़रिए भेजे जाते हैं. ऐसा एआई मॉडल का अनुरोध करने पर होता है. (सोर्स)

कैश एआई मॉडल की क्लाइंट-साइड

एआई मॉडल का इस्तेमाल करते समय, उस मॉडल को ब्राउज़र में साफ़ तौर पर कैश मेमोरी में सेव करना ज़रूरी होता है. इससे यह पक्का होता है कि उपयोगकर्ता के ऐप्लिकेशन को फिर से लोड करने के बाद, मॉडल डेटा आसानी से उपलब्ध हो.

ऐसा करने के लिए, कई तकनीकों का इस्तेमाल किया जा सकता है. यहां दिए गए कोड सैंपल के लिए, मान लें कि हर मॉडल फ़ाइल को मेमोरी में blob नाम वाले Blob ऑब्जेक्ट में सेव किया गया है.

परफ़ॉर्मेंस को समझने के लिए, हर कोड सैंपल को performance.mark() और performance.measure() के तरीकों की मदद से दिखाया जाता है. ये तरीके, डिवाइस पर निर्भर करते हैं. साथ ही, इन्हें सभी के लिए सामान्य नहीं बनाया जा सकता.

Chrome DevTools ऐप्लिकेशन > स्टोरेज में, IndexedDB, कैश मेमोरी, और फ़ाइल सिस्टम के लिए सेगमेंट के साथ इस्तेमाल का डायग्राम देखें. दिखाया गया है कि हर सेगमेंट 1354 मेगाबाइट डेटा खर्च करता है. इसका कुल डेटा 4063 मेगाबाइट है.

ब्राउज़र में एआई मॉडल को कैश मेमोरी में सेव करने के लिए, इनमें से किसी एक एपीआई का इस्तेमाल किया जा सकता है: कैश एपीआई, Origin Private File System API, और IndexedDB API. आम तौर पर, कैश एपीआई इस्तेमाल करने का सुझाव दिया जाता है. हालांकि, इस गाइड में सभी विकल्पों के फ़ायदों और नुकसानों के बारे में बताया गया है.

कैश एपीआई

कैश एपीआई, Request और Response ऑब्जेक्ट के ऐसे पेयर के लिए स्थायी स्टोरेज उपलब्ध कराता है जो लंबे समय तक चलने वाली मेमोरी में कैश किए जाते हैं. हालांकि, सर्विस वर्कर स्पेसिफ़िकेशन में, इसकी जानकारी दी गई है, फिर भी इस एपीआई का इस्तेमाल मुख्य थ्रेड या किसी सामान्य वर्कर से किया जा सकता है. सर्विस वर्कर के कॉन्टेक्स्ट के अलावा किसी अन्य तरीके से इसका इस्तेमाल करने के लिए, सिंथेटिक Response ऑब्जेक्ट के साथ Cache.put() तरीके को कॉल करें. साथ ही, इसे Request ऑब्जेक्ट के बजाय, सिंथेटिक यूआरएल से जोड़ें.

इस गाइड में, मेमोरी में मौजूद blob को शामिल किया गया है. कैश कुंजी के तौर पर नकली यूआरएल और blob के आधार पर बनाए गए सिंथेटिक Response का इस्तेमाल करें. मॉडल को सीधे तौर पर डाउनलोड करने पर, fetch() का अनुरोध करने पर मिलने वाले Response का इस्तेमाल किया जाएगा.

उदाहरण के लिए, कैश एपीआई की मदद से, मॉडल फ़ाइल को सेव और वापस लाने का तरीका यहां बताया गया है.

const storeFileInSWCache = async (blob) => {
  try {
    performance.mark('start-sw-cache-cache');
    const modelCache = await caches.open('models');
    await modelCache.put('model.bin', new Response(blob));
    performance.mark('end-sw-cache-cache');

    const mark = performance.measure(
      'sw-cache-cache',
      'start-sw-cache-cache',
      'end-sw-cache-cache'
    );
    console.log('Model file cached in sw-cache.', mark.name, mark.duration.toFixed(2));
  } catch (err) {
    console.error(err.name, err.message);
  }
};

const restoreFileFromSWCache = async () => {
  try {
    performance.mark('start-sw-cache-restore');
    const modelCache = await caches.open('models');
    const response = await modelCache.match('model.bin');
    if (!response) {
      throw new Error(`File model.bin not found in sw-cache.`);
    }
    const file = await response.blob();
    performance.mark('end-sw-cache-restore');
    const mark = performance.measure(
      'sw-cache-restore',
      'start-sw-cache-restore',
      'end-sw-cache-restore'
    );
    console.log(mark.name, mark.duration.toFixed(2));
    console.log('Cached model file found in sw-cache.');
    return file;
  } catch (err) {    
    throw err;
  }
};

ऑरिजिन प्राइवेट फ़ाइल सिस्टम एपीआई

ऑरिजिन प्राइवेट फ़ाइल सिस्टम (ओपीएफ़एस), स्टोरेज एंडपॉइंट के लिए, बाकी प्लैटफ़ॉर्म की तुलना में एक नया स्टैंडर्ड है. यह पेज के ऑरिजिन के लिए निजी होता है और उपयोगकर्ता को नहीं दिखता, सामान्य फ़ाइल सिस्टम से अलग. इससे एक खास फ़ाइल का ऐक्सेस मिलता है, जो परफ़ॉर्मेंस के लिए बेहतर तरीके से ऑप्टिमाइज़ की गई है. साथ ही, इससे कॉन्टेंट में लिखने का ऐक्सेस भी मिलता है.

उदाहरण के लिए, OPFS में मॉडल फ़ाइल को सेव और वापस लाने का तरीका यहां बताया गया है.

const storeFileInOPFS = async (blob) => {
  try {
    performance.mark('start-opfs-cache');
    const root = await navigator.storage.getDirectory();
    const handle = await root.getFileHandle('model.bin', { create: true });
    const writable = await handle.createWritable();
    await blob.stream().pipeTo(writable);
    performance.mark('end-opfs-cache');
    const mark = performance.measure(
      'opfs-cache',
      'start-opfs-cache',
      'end-opfs-cache'
    );
    console.log('Model file cached in OPFS.', mark.name, mark.duration.toFixed(2));
  } catch (err) {
    console.error(err.name, err.message);
  }
};

const restoreFileFromOPFS = async () => {
  try {
    performance.mark('start-opfs-restore');
    const root = await navigator.storage.getDirectory();
    const handle = await root.getFileHandle('model.bin');
    const file = await handle.getFile();
    performance.mark('end-opfs-restore');
    const mark = performance.measure(
      'opfs-restore',
      'start-opfs-restore',
      'end-opfs-restore'
    );
    console.log('Cached model file found in OPFS.', mark.name, mark.duration.toFixed(2));
    return file;
  } catch (err) {    
    throw err;
  }
};

IndexedDB API

IndexedDB, ब्राउज़र में लगातार आर्बिट्रेरी डेटा को सेव करने के लिए सबसे पहले से मौजूद स्टैंडर्ड है. इसे अपने कुछ जटिल एपीआई के लिए जाना जाता है. हालांकि, idb-keyval जैसी रैपर लाइब्रेरी का इस्तेमाल करके, IndexedDB को एक क्लासिक की-वैल्यू स्टोर की तरह इस्तेमाल किया जा सकता है.

उदाहरण के लिए:

import { get, set } from 'https://cdn.jsdelivr.net/npm/idb-keyval@latest/+esm';

const storeFileInIDB = async (blob) => {
  try {
    performance.mark('start-idb-cache');
    await set('model.bin', blob);
    performance.mark('end-idb-cache');
    const mark = performance.measure(
      'idb-cache',
      'start-idb-cache',
      'end-idb-cache'
    );
    console.log('Model file cached in IDB.', mark.name, mark.duration.toFixed(2));
  } catch (err) {
    console.error(err.name, err.message);
  }
};

const restoreFileFromIDB = async () => {
  try {
    performance.mark('start-idb-restore');
    const file = await get('model.bin');
    if (!file) {
      throw new Error('File model.bin not found in IDB.');
    }
    performance.mark('end-idb-restore');
    const mark = performance.measure(
      'idb-restore',
      'start-idb-restore',
      'end-idb-restore'
    );
    console.log('Cached model file found in IDB.', mark.name, mark.duration.toFixed(2));
    return file;
  } catch (err) {    
    throw err;
  }
};

स्टोरेज को 'जारी है' के तौर पर मार्क करें

स्थायी स्टोरेज के इस्तेमाल की अनुमति मांगने के लिए, कैश मेमोरी में सेव करने के इनमें से किसी भी तरीके के आखिर में navigator.storage.persist() को कॉल करें. इस तरीके से ऐसा प्रॉमिस मिलेगा जो अनुमति मिलने पर true में बदल जाता है. अगर ऐसा नहीं है, तो false होता है. हर ब्राउज़र के लिए तय किए गए नियमों के हिसाब से, ब्राउज़र अनुरोध को पूरा कर सकता है और नहीं भी.

if ('storage' in navigator && 'persist' in navigator.storage) {
  try {
    const persistent = await navigator.storage.persist();
    if (persistent) {
      console.log("Storage will not be cleared except by explicit user action.");
      return;
    }
    console.log("Storage may be cleared under storage pressure.");  
  } catch (err) {
    console.error(err.name, err.message);
  }
}

खास केस: हार्ड डिस्क पर किसी मॉडल का इस्तेमाल करें

ब्राउज़र स्टोरेज के विकल्प के तौर पर, सीधे उपयोगकर्ता की हार्ड डिस्क से एआई मॉडल का रेफ़रंस दिया जा सकता है. इस तकनीक से रिसर्च पर आधारित ऐप्लिकेशन को ब्राउज़र में दिए गए मॉडल को चलाने की क्षमता दिखाने में मदद मिल सकती है या कलाकारों को क्रिएटिविटी के विशेषज्ञ ऐप्लिकेशन में खुद ही ट्रेन किए गए मॉडल इस्तेमाल करने की अनुमति मिल सकती है.

फ़ाइल सिस्टम को ऐक्सेस करने का एपीआई

File System Access API की मदद से हार्ड डिस्क से फ़ाइलें खोलकर, एक FileSystemFileHandle पाएं जिसे IndexedDB पर जारी रखना है.

इस पैटर्न के साथ, उपयोगकर्ता को सिर्फ़ एक बार मॉडल फ़ाइल का ऐक्सेस देना होगा. हमेशा बनी रहने वाली अनुमतियों की मदद से उपयोगकर्ता, फ़ाइल का ऐक्सेस हमेशा के लिए दे सकता है. ऐप्लिकेशन को फिर से लोड करने और माउस क्लिक जैसे उपयोगकर्ता के ज़रूरी जेस्चर को फिर से लोड करने के बाद, FileSystemFileHandle को हार्ड डिस्क पर मौजूद फ़ाइल के ऐक्सेस से, IndexedDB से वापस लाया जा सकता है.

ज़रूरत पड़ने पर, फ़ाइल को ऐक्सेस करने की अनुमतियों के बारे में क्वेरी की जाती है और उनका अनुरोध किया जाता है. इससे, आने वाले समय में फ़ाइल को फिर से लोड करने में आसानी होती है. नीचे दिए गए उदाहरण में, हार्ड डिस्क से किसी फ़ाइल का हैंडल निकालने और फिर हैंडल को सेव करने और उसे वापस लाने का तरीका बताया गया है.

import { fileOpen } from 'https://cdn.jsdelivr.net/npm/browser-fs-access@latest/dist/index.modern.js';
import { get, set } from 'https://cdn.jsdelivr.net/npm/idb-keyval@latest/+esm';

button.addEventListener('click', async () => {
  try {
    const file = await fileOpen({
      extensions: ['.bin'],
      mimeTypes: ['application/octet-stream'],
      description: 'AI model files',
    });
    if (file.handle) {
      // It's an asynchronous method, but no need to await it.
      storeFileHandleInIDB(file.handle);
    }
    return file;
  } catch (err) {
    if (err.name !== 'AbortError') {
      console.error(err.name, err.message);
    }
  }
});

const storeFileHandleInIDB = async (handle) => {
  try {
    performance.mark('start-file-handle-cache');
    await set('model.bin.handle', handle);
    performance.mark('end-file-handle-cache');
    const mark = performance.measure(
      'file-handle-cache',
      'start-file-handle-cache',
      'end-file-handle-cache'
    );
    console.log('Model file handle cached in IDB.', mark.name, mark.duration.toFixed(2));
  } catch (err) {
    console.error(err.name, err.message);
  }
};

const restoreFileFromFileHandle = async () => {
  try {
    performance.mark('start-file-handle-restore');
    const handle = await get('model.bin.handle');
    if (!handle) {
      throw new Error('File handle model.bin.handle not found in IDB.');
    }
    if ((await handle.queryPermission()) !== 'granted') {
      const decision = await handle.requestPermission();
      if (decision === 'denied' || decision === 'prompt') {
        throw new Error(Access to file model.bin.handle not granted.');
      }
    }
    const file = await handle.getFile();
    performance.mark('end-file-handle-restore');
    const mark = performance.measure(
      'file-handle-restore',
      'start-file-handle-restore',
      'end-file-handle-restore'
    );
    console.log('Cached model file handle found in IDB.', mark.name, mark.duration.toFixed(2));
    return file;
  } catch (err) {    
    throw err;
  }
};

ये तरीके अलग-अलग नहीं होते. ऐसा भी हो सकता है कि आप दोनों, ब्राउज़र में मॉडल को साफ़ तौर पर कैश मेमोरी में सेव करें और उपयोगकर्ता की हार्ड डिस्क से मॉडल का इस्तेमाल करें.

डेमो

केस स्टोरेज के तीनों सामान्य तरीकों और MediaPipe LLM डेमो में लागू किए गए हार्ड डिस्क के तरीके को देखा जा सकता है.

बोनस: बड़ी फ़ाइल को कई हिस्सों में डाउनलोड करें

अगर आपको इंटरनेट से कोई बड़ा एआई मॉडल डाउनलोड करना है, तो डाउनलोड को अलग-अलग हिस्सों में एक साथ ले जाएं. इसके बाद, इन्हें क्लाइंट पर फिर से जोड़ें.

यहां एक हेल्पर फ़ंक्शन दिया गया है, जिसे अपने कोड में इस्तेमाल किया जा सकता है. आपको सिर्फ़ इसे url पास करना होगा. ये सभी ज़रूरी नहीं हैं. chunkSize (डिफ़ॉल्ट: 5 एमबी), maxParallelRequests (डिफ़ॉल्ट: 6), progressCallback फ़ंक्शन (जो downloadedBytes और कुल fileSize की रिपोर्ट करता है), और AbortSignal सिग्नल के लिए signal.

अपने प्रोजेक्ट में इस फ़ंक्शन को कॉपी किया जा सकता है या npm पैकेज से fetch-in-chunks पैकेज इंस्टॉल किया जा सकता है.

async function fetchInChunks(
  url,
  chunkSize = 5 * 1024 * 1024,
  maxParallelRequests = 6,
  progressCallback = null,
  signal = null
) {
  // Helper function to get the size of the remote file using a HEAD request
  async function getFileSize(url, signal) {
    const response = await fetch(url, { method: 'HEAD', signal });
    if (!response.ok) {
      throw new Error('Failed to fetch the file size');
    }
    const contentLength = response.headers.get('content-length');
    if (!contentLength) {
      throw new Error('Content-Length header is missing');
    }
    return parseInt(contentLength, 10);
  }

  // Helper function to fetch a chunk of the file
  async function fetchChunk(url, start, end, signal) {
    const response = await fetch(url, {
      headers: { Range: `bytes=${start}-${end}` },
      signal,
    });
    if (!response.ok && response.status !== 206) {
      throw new Error('Failed to fetch chunk');
    }
    return await response.arrayBuffer();
  }

  // Helper function to download chunks with parallelism
  async function downloadChunks(
    url,
    fileSize,
    chunkSize,
    maxParallelRequests,
    progressCallback,
    signal
  ) {
    let chunks = [];
    let queue = [];
    let start = 0;
    let downloadedBytes = 0;

    // Function to process the queue
    async function processQueue() {
      while (start < fileSize) {
        if (queue.length < maxParallelRequests) {
          let end = Math.min(start + chunkSize - 1, fileSize - 1);
          let promise = fetchChunk(url, start, end, signal)
            .then((chunk) => {
              chunks.push({ start, chunk });
              downloadedBytes += chunk.byteLength;

              // Update progress if callback is provided
              if (progressCallback) {
                progressCallback(downloadedBytes, fileSize);
              }

              // Remove this promise from the queue when it resolves
              queue = queue.filter((p) => p !== promise);
            })
            .catch((err) => {              
              throw err;              
            });
          queue.push(promise);
          start += chunkSize;
        }
        // Wait for at least one promise to resolve before continuing
        if (queue.length >= maxParallelRequests) {
          await Promise.race(queue);
        }
      }

      // Wait for all remaining promises to resolve
      await Promise.all(queue);
    }

    await processQueue();

    return chunks.sort((a, b) => a.start - b.start).map((chunk) => chunk.chunk);
  }

  // Get the file size
  const fileSize = await getFileSize(url, signal);

  // Download the file in chunks
  const chunks = await downloadChunks(
    url,
    fileSize,
    chunkSize,
    maxParallelRequests,
    progressCallback,
    signal
  );

  // Stitch the chunks together
  const blob = new Blob(chunks);

  return blob;
}

export default fetchInChunks;

अपने लिए सही तरीका चुनें

इस गाइड में, ब्राउज़र में एआई मॉडल को असरदार तरीके से कैश मेमोरी में सेव करने के कई तरीके बताए गए हैं. यह एक ऐसा टास्क है जो आपके ऐप्लिकेशन की परफ़ॉर्मेंस और उपयोगकर्ता के अनुभव को बेहतर बनाने के लिए बहुत ज़रूरी है. Chrome की स्टोरेज टीम, बेहतरीन परफ़ॉर्मेंस के लिए कैश एपीआई का सुझाव देती है. इससे एआई मॉडल का तुरंत ऐक्सेस मिलता है, लोड होने में लगने वाला समय कम होता है, और रिस्पॉन्स बेहतर होता है.

OPFS और IndexedDB कम इस्तेमाल किए जा सकने वाले विकल्प हैं. डेटा को सेव करने से पहले, OPFS और IndexedDB API को डेटा को क्रम से लगाना ज़रूरी है. इंडेक्स किए गए डेटा को फिर से हासिल करने के बाद, उसे डीसीरियलाइज़ (पार्स) करना पड़ता है. इस वजह से, यह बड़े मॉडल स्टोर करने के लिए सबसे खराब जगह बन जाती है.

खास ऐप्लिकेशन के लिए, File System Access API, उपयोगकर्ता के डिवाइस पर फ़ाइलों को सीधे तौर पर ऐक्सेस करने की सुविधा देता है. यह उन उपयोगकर्ताओं के लिए सही है जो अपने एआई मॉडल खुद मैनेज करते हैं.

अगर आपको अपने एआई मॉडल को सुरक्षित रखना है, तो इसे सर्वर पर रखें. क्लाइंट पर सेव हो जाने के बाद, DevTools या OFPS DevTools एक्सटेंशन की मदद से कैश और IndexedDB, दोनों से डेटा निकालना आसान नहीं है. ये Storage API साफ़ तौर पर सुरक्षा के बराबर होते हैं. ऐसा हो सकता है कि आप मॉडल के एन्क्रिप्ट (सुरक्षित) किए गए वर्शन को सेव करना चाहें, लेकिन इसके बाद आपको क्लाइंट से डिक्रिप्शन कुंजी लेनी होगी. इससे, इस मॉडल को बीच में ही रोका जा सकता है. इसका मतलब है कि किसी बैड ऐक्टर की आपके मॉडल को चुराने की कोशिश थोड़ी मुश्किल होती है, लेकिन नामुमकिन नहीं.

हमारा सुझाव है कि आप कैश मेमोरी में डेटा सेव करने की ऐसी रणनीति चुनें जो आपके ऐप्लिकेशन की ज़रूरी शर्तों, टारगेट ऑडियंस के व्यवहार, और इस्तेमाल किए जाने वाले एआई मॉडल की विशेषताओं के हिसाब से सही हो. इससे यह पक्का होता है कि आपके ऐप्लिकेशन, नेटवर्क की अलग-अलग स्थितियों और सिस्टम से जुड़ी पाबंदियों के हिसाब से काम करते हैं.


स्वीकार की गई

इसकी समीक्षा जोशुआ बेल, रेइली ग्रांट, इवान स्टेड, नैथन मेमोरीमट, ऑस्टिन सुलिवन, एटिएन नोएल, आंद्रे बंडारा, एलेक्ज़ेंड्रा क्लेपर, फ़्रैंकोइस बोफ़र्ट, पॉल किनलान, और रेचल एंड्रयू ने की है.